Рубрики
Без рубрики

Одностраничные приложения с Vue.js и колба: RESTful API с колбой

Автор оригинала: Adam McQuistan.

RESTful API с колбой

Добро пожаловать в четвертый пост об использовании Vue.js и колба для полнотекстовой веб-разработки. Основное внимание в этом посте будет уделено созданию бэкэнд-REST API с использованием веб-фреймворка Flask на основе Python.

Код для этого поста находится в репо на моем аккаунте GitHub под веткой Четвертый пост .

Содержание серии

  1. Настройка и знакомство с VueJS
  2. Навигация по маршрутизатору Vue
  3. Государственное управление с помощью Vuex
  4. RESTful API с колбой (вы здесь)
  5. Интеграция AJAX с REST API
  6. Аутентификация JWT
  7. Развертывание на виртуальном частном сервере

Краткое описание колбы

Flask-это микро-фреймворк на основе Python для быстрого прототипирования и разработки веб-приложений малого и среднего размера. Колба уже была освещена в нескольких предыдущих постах здесь и здесь на StackAbuse, поэтому я не буду вдаваться в подробности, относящиеся к основным или общим частям колбы. Вместо этого я буду использовать более прагматичный подход, сосредоточившись в основном на построении RESTful API для подпитки интерфейса данными, о чем я рассказывал в статьях, предшествующих этой.

Строительные леса из файлов Бэкэнд проекта

Я начинаю с каталога/backend, создавая виртуальную среду Python3 и устанавливая Flask и несколько других необходимых библиотек.

$ python -m venv venv
$ source venv/bin/activate
(venv) $ pip install Flask Flask-SQLAlchemy Flask-Migrate Flask-Script requests

Одна вещь, которая делает Flask (и в значительной степени всю экосистему Python) настолько потрясающей,-это большое количество хорошо разработанных пакетов, доступных на PyPI . Ниже приводится краткое описание библиотек, которые я установил, и их предполагаемое использование.

  • Колба: Веб-микро-фреймворк
  • Flask-SQLAlchemy: ROM на основе SQLAlchemy с некоторым специфичным для Flash потрясающим соусом, упакованным вместе с ним
  • Flask-Migrate: Библиотека миграции баз данных
  • Flask-Script: Чрезвычайно полезный пакет для взаимодействия с приложением Flask из командной строки
  • запросы: удобный пакет для создания сетевых запросов, который я буду использовать для тестирования REST API

В каталоге/backend я создаю несколько новых файлов под названием manage.пи и appserver.py. Кроме того, я создам новый каталог внутри/backend, который станет моим приложением Flask “survey api”. В каталоге survey api я делаю файлы __init__.py, models.py, application.py, и api.py. Это приводит к тому, что структура каталогов начинается с/backend примерно так (опуская каталог venv).

├── manage.py
├── appserver.py
└── surveyapi
    ├── __init__.py
    ├── api.py
    ├── application.py
    ├── config.py
    └── models.py

Ниже приводится краткое описание того, для чего будет использоваться каждый файл:

  • manage.py: доступ к экземпляру приложения Flask для различных команд Flask-Script
  • appserver.py: сценарий запуска для запуска приложения survey api
  • survey api/: приложение backend Flask
  • ____init__.py: превращает каталог survey api в допустимый пакет Python
  • api.py: для определения конечных точек маршрута REST API, способных потреблять и производить запросы и ответы JSON
  • application.py: для создания экземпляра приложения Flask
  • config.py: содержит параметры конфигурации для приложения Flask
  • models.py: для определения классов, которые будут служить объектами данных для приложения опроса, таких как Опрос, Вопрос и Выбор.

Создание фабрики приложений

Я начну кодировать приложение survey api, определив некоторые настройки внутри него. config.py вот так:

"""
config.py
- settings for the flask application object
"""

class BaseConfig(object):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///survey.db'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    # used for encryption and session management
    SECRET_KEY = 'mysecretkey'

Этот класс конфигурации определяет URI подключения базы данных приложения SQLALCHEMY_DATABASE_URL к одному файлу SQLite database под названием survey.db. Он также предоставляет опцию SECRET_KEY config, которая используется для шифрования.

Внутри приложения.py Я создам то, что известно как функция фабрики приложений, которая делает именно то, что звучит, она создает экземпляр приложения Flask. В дополнение к созданию экземпляра Flask он также создает источник объекта Base Config и регистрирует схему маршрутов API, которую я сделаю далее.

"""
application.py
- creates a Flask app instance and registers the database object
"""

from flask import Flask

def create_app(app_name='SURVEY_API'):
    app = Flask(app_name)
    app.config.from_object('surveyapi.config.BaseConfig')
    from surveyapi.api import api
    app.register_blueprint(api, url_prefix="/api")
    return app

Blueprint API

Далее я перееду в api.py модуль, в котором я могу определить Blueprint объект называется api , содержащий RESTful routes. Чтобы все было просто, я начну с определения простой функции представления с именем say_hello () , связанной с конечной точкой api/hello// . Часть URL-адреса представляет собой динамическую строковую переменную, которая передается функции представления say_hello(name) в качестве параметра функции, который я использую в возвращаемом ответном сообщении JSON.

"""
api.py
- provides the API endpoints for consuming and producing
  REST requests and responses
"""

from flask import Blueprint, jsonify, request

api = Blueprint('api', __name__)

@api.route('/hello//')
def say_hello(name):
    response = { 'msg': "Hello {}".format(name) }
    return jsonify(response)

Точка входа Dev Server и проверка настройки

Чтобы проверить это, мне нужно добавить пару строк кода appserver.py чтобы создать экземпляр приложения. Это позволяет мне запустить сервер разработки Flask, вызвав метод run() в экземпляре app .

"""
appserver.py
- creates an application instance and runs the dev server
"""

if __name__ == '__main__':
    from surveyapi.application import create_app
    app = create_app()
    app.run()

Чтобы запустить сервер Flask dev, все, что мне нужно сделать, это запустить интерпретатор Python и скормить ему appserver.py сценарий, как показано ниже.

(venv) $ python appserver.py
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 676-284-544

Как протестировать новую конечную точку, в новом терминале с активированной виртуальной средой я запущу интерпретатор Python и сделаю запрос GET к http://localhost:5000/api/hello/adam/ использование пакета requests .

(venv) $ python
>>> import requests
>>> response = requests.get('http://localhost:5000/api/hello/adam/')
>>> print(response.json())
{'msg': 'Hello adam'}

Определение уровня данных

Теперь, когда я убедился, что у меня есть функционирующее приложение Flask, я могу сосредоточиться на построении слоя данных с помощью ORM Flask-SQLAlchemy. Реализация слоя данных потребует написания некоторых классов данных внутри него. models.py такие как:

  • Опрос: это объект верхнего уровня, который будет содержать один или несколько вопросов вместе с их выбором
  • Вопрос: объекты, принадлежащие объекту опроса и содержащие варианты выбора
  • Выбор: объекты, принадлежащие вопросу и представляющие выбор для вопроса опроса.

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

"""
models.py
- Data classes for the surveyapi application
"""

from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Survey(db.Model):
    __tablename__ = 'surveys'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    questions = db.relationship('Question', backref="survey", lazy=False)

    def to_dict(self):
        return dict(id=self.id,
                    name=self.name,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    questions=[question.to_dict() for question in self.questions])

class Question(db.Model):
    __tablename__ = 'questions'

    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(500), nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    survey_id = db.Column(db.Integer, db.ForeignKey('surveys.id'))
    choices = db.relationship('Choice', backref='question', lazy=False)

    def to_dict(self):
        return dict(id=self.id,
                    text=self.text,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    survey_id=self.survey_id,
                    choices=[choice.to_dict() for choice in self.choices])

class Choice(db.Model):
    __tablename__ = 'choices'

    id = db.Column(db.Integer, primary_key=True)
    text = db.Column(db.String(100), nullable=False)
    selected = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    question_id = db.Column(db.Integer, db.ForeignKey('questions.id'))

    def to_dict(self):
        return dict(id=self.id,
                    text=self.text,
                    created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'),
                    question_id=self.question_id)

Как уже упоминалось ранее, я использую специфичное для Flask расширение SQLAlchemy под названием Flask-SQLAlchemy для питания ORM для этого приложения. Мне нравится Flask-SQLAlchemy, потому что он имеет довольно Pythonic API и предоставляет разумные значения по умолчанию для определения и работы с классами данных.

Каждый класс наследуется от базового класса Sqlalchemy Model , который предоставляет интуитивно понятные и читаемые служебные методы для взаимодействия с данными, хранящимися в базе данных. Кроме того, каждый класс состоит из ряда полей класса, которые преобразуются в поля таблицы базы данных в соответствии с классом SQLAlchemy Column и связанным с ним типом (т. е. Integer , String , DateTime , Text , …).

Вы также заметите, что каждый класс имеет общий метод to_dict () . Этот метод пригодится для сериализации данных моделей в JSON при отправке их по проводу интерфейсному клиенту.

Далее в списке нужно зарегистрировать объект SQLAlchemy, db , с объектом приложения Flask в application.py.

"""
application.py
- creates a Flask app instance and registers the database object
"""

from flask import Flask

def create_app(app_name='SURVEY_API'):
    app = Flask(app_name)
    app.config.from_object('surveyapi.config.BaseConfig')

    from surveyapi.api import api
    app.register_blueprint(api, url_prefix="/api")

    from surveyapi.models import db
    db.init_app(app)

    return app

Последнее, что я хотел бы сделать, это объединить пакеты расширения Flask-Script и Flask-Migrate внутри manage.py модуль для включения миграции. Этот удобный модуль, manage.py, соберет воедино классы данных, которые я только что определил, и свяжет их с контекстом приложения вместе с механизмами Flask-Migrate и Flask-Script.

"""
manage.py
- provides a command line utility for interacting with the
  application to perform interactive debugging and setup
"""

from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand

from surveyapi.application import create_app
from surveyapi.models import db, Survey, Question, Choice

app = create_app()

migrate = Migrate(app, db)
manager = Manager(app)

# provide a migration utility command
manager.add_command('db', MigrateCommand)

# enable python shell with application context
@manager.shell
def shell_ctx():
    return dict(app=app,
                db=db,
                Survey=Survey,
                Question=Question,
                Choice=Choice)

if __name__ == '__main__':
    manager.run()

Я выполняю две вещи в этом фрагменте кода выше. Во-первых, я создаю экземпляр объекта приложения Flask, чтобы он предоставлял контекст экземплярам Migrate(app, db) и Manage(app) . Затем я добавляю команду к объекту manager , которая позволяет мне создавать и запускать миграции из командной строки следующим образом:

(venv) $ python manage.py db init
  • Инициализируйте каталог миграций рядом с приложением survey api и файлом базы данных survey.db
(venv) $ python manage.py db migrate
  • Создайте начальный файл миграции для перевода классов в модели.py to SQL, который будет генерировать соответствующие таблицы
(venv) $ python manage.py db upgrade
  • Запустите миграцию, чтобы обновить базу данных с помощью таблиц, описанных на предыдущем шаге

Последнее, что я делаю в manage.py модуль создает другую пользовательскую команду, использующую @manager.shell для украшения функции shell_ctx () , которая возвращает ключевые слова сопоставления dict объектам app и db вместе с классами данных Survey , Question и Choice .

Теперь я воспользуюсь полезностью этой команды утилиты оболочки, чтобы продемонстрировать, как работать с Flask-SQLAlchemy ORM в интерпретаторе python, который она производит.

(venv) $ python manage.py shell
(venv) Adams-MacBook-Pro:backend adammcquistan$ python manage.py shell
>>> survey = Survey(name='Dogs')
>>> question = Question(text='What is your favorite dog?')
>>> question.choices = [Choice(text='Beagle'), Choice(text='Rottweiler'), Choice(text='Labrador')]
>>> question2 = Question(text='What is your second favorite dog?')
>>> question2.choices = [Choice(text='Beagle'), Choice(text='Rottweiler'), Choice(text='Labrador')]
>>> survey.questions = [question, question2]
>>> db.session.add(survey)
>>> db.session.commit()
>>> surveys = Survey.query.all()
>>> for s in surveys:
...     print('Survey(id={}, name={})'.format(s.id, s.name))
...     for q in s.questions:
...             print('  Question(id={}, text={})'.format(q.id, q.text))
...             for c in q.choices:
...                     print('    Choice(id={}, text={})'.format(c.id, c.text))
...
Survey(id=1, name=Dogs)
  Question(id=1, text=What is your favorite dog?)
    Choice(id=1, text=Beagle)
    Choice(id=3, text=Labrador)
    Choice(id=2, text=Rottweiler)
  Question(id=2, text=What is your second favorite dog?)
    Choice(id=4, text=Beagle)
    Choice(id=6, text=Labrador)
    Choice(id=5, text=Rottweiler)

Это довольно ловко, правда?

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

Завершение работы RESTful API

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

  • Получите все опросы вместе с их вопросами и выбором
  • Получите один опрос вместе с его вопросами и вариантами выбора
  • Создайте новый опрос вместе с указанными в нем вопросами и вариантами выбора
  • Обновите варианты ответов на опрос после его проведения

Для начала я буду импортировать все классы данных вместе с экземпляром SQLAlchemy db , чтобы иметь к ним доступ. В верхней части api.py Я добавляю следующий импорт:

"""
api.py
- provides the API endpoints for consuming and producing
  REST requests and responses
"""

from flask import Blueprint, jsonify, request
from .models import db, Survey, Question, Choice

Что касается фактических конечных точек ресурсов, то я начну с кодирования возможности извлечения всех ресурсов опроса. Внутри api.py Мне нужно заменить /hello// endpoint функцией route /surveys/| endpoint и surveys() view.

@api.route('/surveys/')
def surveys():
    surveys = Survey.query.all()
    return jsonify({ 'surveys': [s.to_dict() for s in surveys] })

Если dev-сервер все еще работает, то после того, как я сохраню файлы проекта, сервер должен автоматически перезагрузиться, обновив все изменения. Если нет, то запускаем (venv) $ python appserver.py запустится сервер. Теперь в другом терминале с активированной виртуальной средой я могу использовать пакет requests для тестирования этой новой конечной точки. Тем не менее, я хотел бы поделиться профессиональным советом по отображению ответов JSON более читабельным способом с помощью другого потрясающего пакета Python под названием pprint .

(venv) $ pip install pprint
(venv) $ python
>>> import pprint, requests
>>> pp == pprint.PrettyPrinter()
>>> resp = requests.get('http://localhost:5000/api/surveys/')
>>> pp.pprint(resp.json())
{'surveys': [{
     'created_at': '2018-03-06 03:52:44',
     'id': 1,
     'name': 'Dogs',
     'questions': [{
          'choices': [{
               'created_at': '2018-03-06 03:52:44',
               'id': 1,
               'question_id': 1,
               'text': 'Beagle'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 3,
               'question_id': 1,
               'text': 'Labrador'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 2,
               'question_id': 1,
               'text': 'Rottweiler'}],
            'created_at': '2018-03-06 03:52:44',
            'id': 1,
            'survey_id': 1,
            'text': 'What is your favorite dog?'
         },{
          'choices': [{
              'created_at': '2018-03-06 03:52:44',
              'id': 4,
              'question_id': 2,
              'text': 'Beagle'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 6,
              'question_id': 2,
              'text': 'Labrador'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 5,
              'question_id': 2,
              'text': 'Rottweiler'}],
          'created_at': '2018-03-06 03:52:44',
          'id': 2,
          'survey_id': 1,
          'text': 'What is your second favorite dog?'}]}
    ]}

Далее я буду реализовывать функциональность для извлечения одного опроса по его id с конечной точкой URL /surveys/id/| и функцией просмотра survey(id) . Сразу после функции surveys() API view я помещаю следующий код:

@api.route('/surveys//')
def survey(id):
    survey = Survey.query.get(id)
    return jsonify({ 'survey': survey.to_dict() })

Опять же, я сохраню файлы и протестирую новую конечную точку API, чтобы убедиться, что она подает правильный ответ.

>>> resp = requests.get('http://localhost:5000/api/surveys/1/')
>>> pp.pprint(resp.json())
{'survey': {
     'created_at': '2018-03-06 03:52:44',
     'id': 1,
     'name': 'Dogs',
     'questions': [{
          'choices': [{
               'created_at': '2018-03-06 03:52:44',
               'id': 1,
               'question_id': 1,
               'text': 'Beagle'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 3,
               'question_id': 1,
               'text': 'Labrador'
              },{
               'created_at': '2018-03-06 03:52:44',
               'id': 2,
               'question_id': 1,
               'text': 'Rottweiler'}],
            'created_at': '2018-03-06 03:52:44',
            'id': 1,
            'survey_id': 1,
            'text': 'What is your favorite dog?'
         },{
          'choices': [{
              'created_at': '2018-03-06 03:52:44',
              'id': 4,
              'question_id': 2,
              'text': 'Beagle'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 6,
              'question_id': 2,
              'text': 'Labrador'
             },{
              'created_at': '2018-03-06 03:52:44',
              'id': 5,
              'question_id': 2,
              'text': 'Rottweiler'}],
          'created_at': '2018-03-06 03:52:44',
          'id': 2,
          'survey_id': 1,
          'text': 'What is your second favorite dog?'}]}
    }

До сих пор я использовал только стандартный метод HTTP GET route, подходящий для извлечения данных из RESTful API. Однако для последних двух частей функциональности мне нужно будет использовать HTTP POST и PUT методы для конечных точек /api/surveys/| и | api/surveys/id//соответственно. Я буду использовать метод HTTP POST для создания новых опросов и метод HTTP PUT для обновления существующего опроса новым набором выбранных вариантов ответов.

Для маршрута /api/surveys/ мне нужно будет добавить параметр method в объявление маршрута, чтобы указать, что он принимает как GET, так и POST методы, methods=('GET','POST') . Кроме того, я изменю тело функции surveys() view, чтобы дифференцировать тип метода и добавить возможность сохранения нового опроса в базу данных.

@api.route('/surveys/', methods=('GET', 'POST'))
def fetch_surveys():
    if request.method == 'GET':
        surveys = Survey.query.all()
        return jsonify({ 'surveys': [s.to_dict() for s in surveys] })
    elif request.method == 'POST':
        data = request.get_json()
        survey = Survey(name=data['name'])
        questions = []
        for q in data['questions']:
            question = Question(text=q['text'])
            question.choices = [Choice(text=c['text'])
                                for c in q['choices']]
            questions.append(question)
        survey.questions = questions
        db.session.add(survey)
        db.session.commit()
        return jsonify(survey.to_dict()), 201

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

>>> import json
>>> survey = {
...   'name': 'Cars',
...   'questions': [{
...     'text': 'What is your favorite car?',
...     'choices': [
...       { 'text': 'Corvette' },
...       { 'text': 'Mustang' },
...       { 'text': 'Camaro' }]
...   }, {
...     'text': 'What is your second favorite car?',
...     'choices': [
...       { 'text': 'Corvette' },
...       { 'text': 'Mustang' },
...       { 'text': 'Camaro' }]
...   }]
... }
>>> headers = {'Content-type': 'application/json'}
>>> resp = requests.post('http://localhost:5000/api/surveys/', headers=headers, data=json.dumps(survey))
>>> resp.status_code
201

Последняя часть, которую нужно реализовать, – это возможность обновить существующий опрос новыми выборками ответов на опрос. Опять же, мне нужно будет добавить методы GET и PUT в /api/surveys/id/ определение маршрута, methods=('GET', 'PUT') . Затем я обновляю функцию survey(id) view, чтобы обновить связанные варианты вопросов опроса, указанные как выбранные в теле JSON запроса PUT.

@api.route('/surveys//', methods=('GET', 'PUT'))
def survey(id):
    if request.method == 'GET':
        survey = Survey.query.get(id)
        return jsonify({ 'survey': survey.to_dict() })
    elif request.method == 'PUT':
        data = request.get_json()
        for q in data['questions']:
            choice = Choice.query.get(q['choice'])
            choice.selected = choice.selected + 1
        db.session.commit()
        survey = Survey.query.get(data['id'])
        return jsonify(survey.to_dict()), 201

Наконец, мне нужно сохранить все мои файлы и сделать один последний тест, например:

>>> survey_choices = {
...   'id': 1,
...   'name': 'Dogs',
...   'questions': [
...     { 'id': 1, 'choice': 1 },
...     { 'id': 2, 'choice': 5 }]
... }
>>> headers = {'Content-type': 'application/json'}
>>> resp = requests.put('http://localhost:5000/api/surveys/1/', data=json.dumps(survey_choices), headers=headers)
>>> resp.status_code()
201

Ресурсы

Хотите узнать больше о Python и создании бэкенд-API? Попробуйте проверить курс, например, для более глубокого погружения в бэкэнд-веб-разработку с помощью Python.

Вывод

В этой статье я рассказал о том, как реализовать простой, довольно голый, RESTful API с помощью Flask в соответствии со следующей таблицей:

/api/опросы/ ПОЛУЧИТЬ Получить все опросы
/api/опросы/ ПОСТ Создание нового опроса
/api/опросы/id/ ПОЛУЧИТЬ Получение опроса по идентификатору
/api/опросы/id/ КЛАСТЬ Обновление вариантов выбора опроса

В следующей статье я продемонстрирую, как интегрировать интерфейс Vue.js приложение, чтобы оно могло потреблять и отправлять обновления данных в бэкэнд Flask.

Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.