Python REST API: Flask, Connexion и SQLAlchemy (часть 3)

от автора

Это перевод статьи от Philipp Acsany

Большинство современных веб-приложений работают на основе REST API — методологии, позволяющей разработчикам отделить разработку пользовательского интерфейса (FrontEnd) от разработки внутренней серверной логики (BackEnd), а пользователи получают интерфейс с динамически подгружаемыми данными. В этой серии из трех частей вы создадите REST API с помощью веб-фреймворка Flask.

В первой части и второй части руководства вы создали базовый проект Flask и добавили конечные точки, которые вы подключили к базе данных SQLite, используя сериализацию и десериализацию JSON в объекты Python с помощью Marshmallow. Вы также дополнили свой API новыми возможностями, для которых создали аннотации с помощью Swagger UI API.

В этой третьей части серии вы узнаете, как:
— Работать с несколькими таблицами с взаимосвязанной информацией в базе данных
— Создавать связи «один ко многим» в базе данных
— Управлять связями с помощью SQLAlchemy
— Сериализовать сложные схемы данных со связями с помощью Marshmallow
— Отображать связанные объекты в клиентском интерфейсе

Техзадание

Создать приложение для управления открытками персонажам, от которых вы можете получить подарки в течение года. Вот эти сказочные лица: Tooth Fairy (Зубная фея), Easter Bunny (Пасхальный кролик) и Knecht Ruprecht (Кнехт Рупрехт).

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

В этой третьей части вы еще больше расширите свой инструментарий программирования. Вы узнаете, как создавать иерархические структуры данных, представленные в виде отношений «один ко многим» с помощью SQLAlchemy. Кроме того, вы также расширите API REST, который вы уже создали, для создания, чтения, обновления и удаления открыток для персонажей.

Пришло время завершить это руководство из трех частей, создав отношения между персонажами и открытками!

План части 3

В первой части вы начали создавать REST API с хранением данных в словаре PEOPLE. Затем, во второй части вы реализовали подключение REST API к базе данных для сохранения данных в промежутках между запусками приложения.

Вы добавили возможность сохранять изменения, внесенные через REST API, в базу данных с помощью SQLAlchemy и узнали, как сериализовать эти данные для REST API с помощью Marshmallow.

В настоящее время база данных people.db содержит только данные о персонажах. В этой части серии вы добавите новую таблицу для хранения открыток. Чтобы связать открытки с персонажем, которому они предназначены, вы создадите связи между записями таблицы person и таблицы note в базе данных.

Вы пересоздадите базу данных people.db с помощью скрипта create_db.py, который также содержит некоторые данные о персонажах и открытках. Вот фрагмент набора данных, с которым вы будете работать:

PEOPLE_NOTES = [     {         "lname": "Fairy",         "fname": "Tooth",         "notes": [             ("I brush my teeth after each meal.", "2022-01-06 17:10:24"),             ("The other day a friend said I have big teeth.", "2022-03-05 22:17:54"),             ("Do you pay per gram?", "2022-03-05 22:18:10"),         ],     },    # ... ]

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

Наконец, вы отобразите содержимое своей базы данных на домашней странице своего приложения и будете использовать API Flask REST для добавления, обновления и удаления открыток, которые вы отправляете персонажам.

Поехали!

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

Прежде чем продолжить, убедитесь, что структура вашего проекта выглядит следующим образом:

rp_flask_api/ ├── templates/ │   └── home.html ├── venv/ # виртуальное окружение ├── app.py ├── config.py ├── create_db.py # файл создания и первоначального заполнения базы данных ├── models.py ├── people.db # файл базы данных ├── people.py ├── requirements.txt # файл с указанием зависимостей, установка через pip install -r requirements.txt └── swagger.yml

Добавление зависимостей

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

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

  • Windows

python -m venv venv .\venv\Scripts\activate
  • Linux + macOS

python -m venv venv source venv/bin/activate

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

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

Теперь вы можете убедиться, что ваше приложение Flask работает без ошибок: выполните следующую команду в каталоге, содержащем файл app.py:

(venv) $ python app.py

При запуске этого приложения веб-сервер запустится на порту 8000. Если вы откроете браузер и перейдете по адресу http://localhost:8000, вы должны увидеть страницу с заголовком Hello, People! и списком персонажей из базы данных:

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

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

Подготовка набора данных

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

Таблица person вашей базы данных в файле people.db в настоящее время выглядит следующим образом:

id

lname

fname

timestamp

1

Fairy

Tooth

2022-10-08 09:15:10

2

Ruprecht

Knecht

2022-10-08 09:15:13

3

Bunny

Easter

2022-10-08 09:15:27

Вы будете расширять свою базу данных списком открыток PEOPLE_NOTES:

PEOPLE_NOTES = [     {         "lname": "Fairy",         "fname": "Tooth",         "notes": [             ("I brush my teeth after each meal.", "2022-01-06 17:10:24"),             ("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),             ("Do you pay per gram?", "2022-03-05 22:18:10"),         ],     },     {         "lname": "Ruprecht",         "fname": "Knecht",         "notes": [             ("I swear, I'll do better this year.", "2022-01-01 09:15:03"),             ("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),         ],     },     {         "lname": "Bunny",         "fname": "Easter",         "notes": [             ("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),             ("No need to hide the eggs this time.", "2022-04-06 13:03:17"),         ],     }, ]

Обратите внимание, что значения lname в PEOPLE_NOTES соответствуют содержимому вашего столбца lname в таблице person вашей базы данных people.db.

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

Каждый отдельный персонаж связан с несколькими открытками, и каждая отдельная открытка связана только с одним персонажем. Эта иерархия данных известна как отношение «один ко многим», где один родительский объект связан со многими дочерними объектами. Вы увидите, как это отношение «один ко многим» реализуется в базе данных при помощью SQLAlchemy далее в этой статье.

Создание связей открыток с персонажами

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

Для таблицы person никаких изменений не будет. Чтобы представить новую информацию об открытке, вы создадите новую таблицу с именем note. Таблица открыток будет выглядеть следующим образом:

id

person_id

content

timestamp

1

1

I brush my teeth after each meal.

2022-01-06 17:10:24

2

1

The other day a friend said, I have big teeth.

2022-03-05 22:17:54

3

1

Do you pay per gram?

2022-03-05 22:18:10

4

2

I swear, I’ll do better this year.

2022-01-01 09:15:03

5

2

Really! Only good deeds from now on!

2022-02-06 13:09:21

6

3

Please keep the current inflation rate in mind!

2022-01-07 22:47:54

7

3

No need to hide the eggs this time.

2022-04-06 13:03:17

Обратите внимание, что, как и таблица person, таблица note имеет уникальный идентификатор id, который является первичным ключом для таблицы note. Столбец person_id создает связь с таблицей person.

В то время как id является первичным ключом для таблицы, person_id — это то, что известно, как внешний ключ. Внешний ключ дает каждой записи в таблице note указатель на первичный ключ записи person, с которым она связана. Используя это, SQLAlchemy может собрать все открытки, связанные с каждым персонажем, связав первичный ключ person.id с внешним ключом note.person_id.

Почему бы не ограничиться одной таблицей?

База данных, которую вы создали, хранила данные в таблице, а таблица — это двумерный массив строк и столбцов. Можно ли представить словарь People выше в виде одной таблицы строк и столбцов? Это можно сделать следующим образом в таблице базы данных person:

id

lname

fname

timestamp

content

note_timestamp

1

Fairy

Tooth

2022-10-08 09:15:10

I brush my teeth after each meal.

2022-01-06 17:10:24

2

Fairy

Tooth

2022-10-08 09:15:10

The other day a friend said, I have big teeth.

2022-03-05 22:17:54

3

Fairy

Tooth

2022-10-08 09:15:10

Do you pay per gram?

2022-03-05 22:18:10

4

Ruprecht

Knecht

2022-10-08 09:15:13

I swear, I’ll do better this year.

2022-01-01 09:15:03

5

Ruprecht

Knecht

2022-10-08 09:15:13

Really! Only good deeds from now on!

2022-02-06 13:09:21

6

Easter

Bunny

2022-10-08 09:15:27

Please keep the current inflation rate in mind!

2022-01-07 22:47:54

7

Easter

Bunny

2022-10-08 09:15:27

No need to hide the eggs this time.

2022-04-06 13:03:17

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

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

Хотя структура таблицы выше будет работать, у нее есть некоторые реальные недостатки. К ним относятся следующие:
— Проблемы с использованием из-за избыточных данных
— Нелогичные названия столбцов, отражающие свойства разных сущностей: персонажей и открыток
— Сложность реализации отношений «один ко многим»

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

Наличие таких избыточных данных также может привести к проблемам обслуживания с течением времени. Например, что, если Easter Bunny решил, что пора сменить свое имя? Для этого необходимо обновить каждую запись открыток, содержащих имя Easter Bunny, чтобы данные были согласованными. Такая работа с базой данных может привести к несогласованности данных, особенно, если работа выполняется человеком, вручную выполняющим SQL-запрос.

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

Что, если вы хотите добавить дополнительные отношения «один ко многим» в таблицу персонажей? Например, вы можете решить включить детей персонажа или номера телефонов. У каждого человека может быть несколько детей и несколько номеров телефонов. С помощью словаря Python People выше вы можете сделать это относительно легко, добавив ключи children и phone_numbers с новыми списками, содержащими данные.

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

Необходимость хранения все более крупных и сложных структур данных повышает популярность баз данных NoSQL. Эти системы баз данных позволяют разработчикам эффективно хранить гетерогенные данные, которые не структурированы в таблицах. Если вам интересно узнать о базах данных NoSQL, ознакомьтесь с Python и MongoDB: подключение к базам данных NoSQL.

Наконец, с данными, которые вы получите из приведенной выше структуры таблицы, будет неудобно работать, поскольку это будет просто большой список списков.

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

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

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

Но, хватит теории! В следующем разделе вы создадите модели, которые представляют придуманные вами связи таблиц базы данных.

Расширение вашей базы данных

В этом разделе вы расширите свою базу данных. Вы собираетесь изменить структуру данных People в models.py, чтобы предоставить каждому персонажу список открыток, связанных с ними. Наконец, вы заполните базу данных некоторыми начальными данными.

Создание моделей данных в SQLAlchemy

Чтобы использовать две таблицы выше и использовать связь между ними, вам нужно будет создать модели SQLAlchemy, которые знают об обеих таблицах и связи между ними.

Начните с обновления модели Person в models.py, чтобы включить связь с коллекцией открыток:

# models.py  from datetime import datetime from config import db, ma  class Person(db.Model):     __tablename__ = "person"     id = db.Column(db.Integer, primary_key=True)     lname = db.Column(db.String(32), unique=True)     fname = db.Column(db.String(32))     timestamp = db.Column(         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow     )     notes = db.relationship(         Note,         backref="person",         cascade="all, delete, delete-orphan",         single_parent=True,         order_by="desc(Note.timestamp)"     ) # ...

В этом коде вы создаете новый атрибут в классе Person с именем .notes. Этот новый атрибут .notes определяется в следующих строках кода:
— Вы создаете новый атрибут с именем .notes и устанавливаете его равным экземпляру объекта с именем db.relationship. Этот объект создает отношение, которое вы добавляете к классу Person, и оно создается со всеми параметрами, определенными в следующих строках.
— Параметр Note определяет класс SQLAlchemy, с которым будет связан класс Person. Класс Note еще не определен, поэтому на данный момент он не будет работать. Иногда может быть проще ссылаться на классы как на строки, чтобы избежать проблем с тем, какой класс определен первым. Например, здесь вы можете использовать "Note» вместо Note.
— Параметр backref="person" создает то, что известно как обратная ссылка в объектах Note. Каждый экземпляр Note будет содержать атрибут с именем .person. Атрибут .person ссылается на родительский объект, с которым связан конкретный экземпляр Note. Наличие ссылки на родительский объект (в данном случае Person) в дочернем объекте может быть очень полезным, если ваш код выполняет итерации по открыткам и должен включать информацию о родительском элементе — персонаже.
— Параметр cascade="all, delete, delete-orphan" определяет, как обрабатывать экземпляры Note при внесении изменений в родительский экземпляр Person. Например, когда удаляется объект Person, SQLAlchemy создаст SQL-запрос, необходимый для удаления объекта Person из базы данных. Этот параметр сообщает SQLAlchemy, что также необходимо удалить все экземпляры Note, связанные с ним. Подробнее об этих параметрах можно прочитать в документации SQLAlchemy.
— Параметр single_parent=True требуется, если delete-orphan является частью предыдущего параметра cascade. Это сообщает SQLAlchemy, что нельзя допускать существования потерянного, осиротевшего Note, то есть Note без родительского объекта Person, поскольку у каждого Note есть как минимум один родитель.
— Параметр order_by="desc(Note.timestamp)" сообщает SQLAlchemy, как сортировать экземпляры Note, связанные с объектом Person. При извлечении объекта Person по умолчанию список атрибутов Notes будет содержать объекты Note в неизвестном порядке. Функция SQLAlchemy desc() отсортирует заметки в порядке убывания от самых новых к самым старым, а не в порядке возрастания по умолчанию.

Теперь, когда ваша модель Person имеет новый атрибут .notes, и это представляет отношение «один ко многим» к объектам Note, вам нужно определить модель SQLAlchemy для объекта Note. Поскольку вы ссылаетесь на Note из Person, добавьте новый класс Note прямо перед определением класса Person:

# models.py  from datetime import datetime from config import db, ma  class Note(db.Model):     __tablename__ = "note"     id = db.Column(db.Integer, primary_key=True)     person_id = db.Column(db.Integer, db.ForeignKey("person.id"))     content = db.Column(db.String, nullable=False)     timestamp = db.Column(         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow     )  class Person(db.Model):     # ... # ...

Класс Note определяет атрибуты открытки, как вы узнали из таблицы базы данных открыток выше. С помощью этого кода вы определяете атрибуты:
— Класс Note, наследуемый от db.Model, точно так же, как вы делали это ранее при создании класса Person.
__tablename__ = "note" сообщает классу, какую таблицу базы данных использовать для хранения объектов Note.
— Атрибут .id, определяя его как целочисленное значение и как первичный ключ для объекта Note.
— Атрибут .person_id и определяет его как внешний ключ, связывая класс Note с классом Person с помощью первичного ключа .person.id. Это и атрибут Person.notes — то, как SQLAlchemy знает, что делать при взаимодействии с объектами Person и Note.
— Атрибут .content, который содержит фактический текст открытки. Параметр nullable=False указывает, что содержимое новых открыток не должно быть пустым.
— Атрибут .timestamp, и точно так же, как в классе Person, этот атрибут содержит время создания или обновления для любого конкретного экземпляра Note.

Теперь, когда вы обновили People и создали модель для Note, переходите к обновлению базы данных.

Обновление базы данных

Теперь, когда вы обновили Person и создали модель Note, вы будете использовать их для перестроения базы данных people.db. Для этого переделайте вспомогательный скрипт Python с именем create_db.py:

# create_db.py  from datetime import datetime from config import app, db from models import Person, Note  PEOPLE_NOTES = [     {         "lname": "Fairy",         "fname": "Tooth",         "notes": [             ("I brush my teeth after each meal.", "2022-01-06 17:10:24"),             ("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),             ("Do you pay per gram?", "2022-03-05 22:18:10"),         ],     },     {         "lname": "Ruprecht",         "fname": "Knecht",         "notes": [             ("I swear, I'll do better this year.", "2022-01-01 09:15:03"),             ("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),         ],     },     {         "lname": "Bunny",         "fname": "Easter",         "notes": [             ("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),             ("No need to hide the eggs this time.", "2022-04-06 13:03:17"),         ],     }, ]  with app.app_context():     db.drop_all()     db.create_all()     for data in PEOPLE_NOTES:         new_person = Person(lname=data.get("lname"), fname=data.get("fname"))         for content, timestamp in data.get("notes", []):             new_person.notes.append(                 Note(                     content=content,                     timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"),                 )             )         db.session.add(new_person)     db.session.commit()

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

При выполнении create_db.py вы заново создадите people.db. Все существующие данные в people.db будут потеряны.

Запуск программы create_db.py из командной строки заново создаст базу данных с новыми дополнениями, подготовив ее к использованию в приложении:

(venv) $ python create_db.py

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

Отображение персонажей с их открытками

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

Отображение заметок в веб-интерфейсе

В предыдущем разделе вы создали связь между персонажем и его открытками, добавив атрибут .notes в класс Person. Обновите home.html в папке templates/, чтобы отобразить открытки персонажей:

<!-- templates/home.html -->  <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <title>RP Flask REST API</title> </head> <body>     <h1>Hello, People!</h1>     {% for person in people %}     <h2>{{ person.fname }} {{ person.lname }}</h2>     <ul>         {% for note in person.notes %}         <li>{{ note.content }}</li>         {% endfor %}     </ul>     {% endfor %} </body> </html>

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

Перейдите по адресу http://localhost:8000, чтобы проверить, отображается ли ваш шаблон так, как ожидалось:

Отлично, вы можете видеть открытки от каждого персонажа, перечисленные в пользовательском интерфейсе. Это означает, что Flask успешно соединяет Person и Notes под капотом и предоставляет вам объект people, с которым вы можете удобно работать.

Отлично, вы можете видеть открытки от каждого персонажа, перечисленные в пользовательском интерфейсе. Это означает, что Flask успешно соединяет Person и Notes под капотом и предоставляет вам объект people, с которым вы можете удобно работать.

Составление запросов к API открыток

Далее проверьте конечную точку /api/people вашего API по адресу http://localhost:8000/api/people:

Вы получаете коллекцию персонажей без ошибок, однако, в полученных данных нет никаких открыток.

Вы получаете коллекцию персонажей без ошибок, однако, в полученных данных нет никаких открыток.

Чтобы выявить проблему, посмотрите на read_all() в people.py:

# people.py  # ... def read_all():     people = Person.query.all()     person_schema = PersonSchema(many=True)     return person_schema.dump(people) # ...

Метод .dump() работает с тем, что получает, и не отфильтровывает никакие данные. Так что проблема может быть в определении people или person_schema.

Вызов запроса к базе данных для заполнения people точно такой же, как в app.py:

Person.query.all()

Этот вызов успешно отработал на фронтенде, чтобы показать открытки для каждого человека. Это выделяет PersonSchema как наиболее вероятного виновника.

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

Откройте models.py и обновите PersonSchema:

# models.py  # ...  class PersonSchema(ma.SQLAlchemyAutoSchema):     class Meta:         model = Person         load_instance = True         sqla_session = db.session         include_relationships = True

С параметром include_relationships в классе Meta PersonSchema вы сообщаете Marshmallow о необходимости добавить любые связанные объекты в схему данных person. Однако результат все еще не выглядит списком открыток, как ожидалось:

Ответ по адресу http://localhost:8000/api/people теперь содержит открытки каждого персонажа. Но вместо того, чтобы показывать все данные, которые содержит открытка, объект открытки содержит только список первичных ключей.

Ответ по адресу http://localhost:8000/api/people теперь содержит открытки каждого персонажа. Но вместо того, чтобы показывать все данные, которые содержит открытка, объект открытки содержит только список первичных ключей.

Создание схемы данных для открытки

Ваш API в ответе перечислил только первичные ключи открыток каждого персонажа. Это справедливо, потому что вы еще не объявили, как Marshmallow должен десериализовать открытки.

Помогите Marshmallow, создав NoteSchema в models.py ниже Note и выше Person:

# models.py  # ... class Note(db.Model):     # ...  class NoteSchema(ma.SQLAlchemyAutoSchema):     class Meta:         model = Note         load_instance = True         sqla_session = db.session         include_fk = True  class Person(db.Model):     # ...  class PersonSchema(ma.SQLAlchemyAutoSchema):     # ...  note_schema = NoteSchema() # ...

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

Поскольку ваша модель данных Note содержит внешний ключ, вы должны установить include_fk в True. В противном случае Marshmallow не распознает person_id во время процесса сериализации.

При наличии NoteSchema вы можете ссылаться на него в PeopleSchema:

# models.py  from datetime import datetime from marshmallow_sqlalchemy import fields  from config import db, ma # ... class PersonSchema(ma.SQLAlchemyAutoSchema):     class Meta:         model = Person         load_instance = True         sqla_session = db.session         include_relationships = True      notes = fields.Nested(NoteSchema, many=True)

После импорта fields из marshmallow_sqlalchemy вы можете ссылаться на связанный объект Note по его NoteSchema. Чтобы избежать ошибок, убедитесь, что вы определили NoteSchema выше PeopleSchema.

Хотя вы работаете с SQLAlchemyAutoSchema, вам необходимо явно создать поле notes в PersonSchema. В противном случае Marshmallow не получит всю необходимую информацию для работы с данными Notes. Например, он не будет знать, что вы ожидаете список объектов, используя аргумент many.

С внесенными изменениями проверьте конечную точку вашего API по адресу http://localhost:8000/api/people:

Отлично, ваша функция read_all() возвращает не только всех персонажей, но и все открытки, прикрепленные к каждому персонажу!

Отлично, ваша функция read_all() возвращает не только всех персонажей, но и все открытки, прикрепленные к каждому персонажу!

В следующем разделе вы расширите свой API Flask REST для создания, чтения, обновления и удаления открыток.

Обработка открыток с помощью вашего API REST

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

Действие

HTTP-метод

URL

Описание

Чтение

GET

/api/people

Получение списка персонажей

Чтение

GET

/api/people/<lname>

Получение данных определенного персонажа

Хотя вы можете читать заметки через конечные точки, показанные в таблице выше, в настоящее время нет возможности читать только одну заметку или управлять любыми заметками в вашем REST API.

Параметры URL чувствительны к регистру. Например, вы должны посетить http://localhost:8000/api/people/Ruprecht с заглавной буквой R в фамилии Ruprecht.

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

Действие

HTTP-метод

URL

Описание

Создание

POST

/api/notes

Создает новую открытку

Чтение

GET

/api/notes/<note_id>

Получение данных конкретной открытки

Обновление

PUT

api/notes/<note_id>

Обновление данных конкретной открытки

Удаление

DELETE

api/notes/<note_id>

Удаление конкретной открытки

Вы начнете с добавления функциональности для чтения одной открытки. Для этого вам нужно будет настроить файл конфигурации Swagger, содержащий аннотации API.

Чтение одной открытки

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

Перед добавлением конечной точки обновите конфигурацию Swagger, создав компонент параметра note_id в файле swagger.yml:

# swagger.yml  # ... components:   schemas:     # ...    parameters:     lname:       # ...     note_id:       name: "note_id"       description: "ID of the note"       in: path       required: true       schema:         type: "integer" # ...

Параметр note_id будет частью ваших конечных точек для определения того, какую открытку вы хотите обработать.

Продолжайте редактировать swagger.yml и добавьте данные для конечной точки для чтения одной заметки:

# swagger.yml  # ... paths:   /people:     # ...   /people/{lname}:     # ...   /notes/{note_id}:     get:       operationId: "notes.read_one"       tags:         - Notes       summary: "Read one note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "200":           description: "Successfully read one note"

Структура /notes/{note_id} похожа на /people/{lname}. Вы начинаете с операции get для пути /notes/{note_id}. Подстрока {note_id} является значением для идентификатора заметки, которое вы должны передать как параметр URL. Так, например, URL http://localhost:8000/api/notes/1 предоставит вам данные для заметки с первичным ключом 1.

OperationId указывает на notes.read_one. Это означает, что ваш API ожидает функцию read_one() в файле notes.py. Продолжайте, создайте файл notes.py и добавьте туда функцию read_one():

# notes.py  from flask import abort, make_response  from config import db from models import Note, note_schema  def read_one(note_id):     note = Note.query.get(note_id)      if note is not None:         return note_schema.dump(note)     else:         abort(             404, f"Note with ID {note_id} not found"         )

Хотя вы пока не используете make_response() и db, вы можете продолжить и добавить их в свои импорты. Вы будете использовать их немного позже, когда будете записывать в базу данных.

На данный момент вы только читаете из базы данных с параметром note_id из пути URL REST. Вы используете note_id в методе .get() запроса, чтобы получить заметку с первичным ключом целого числа note_id.

Если открытка найдена, то note содержит объект Note, и вы возвращаете сериализованный объект. Попробуйте, посетив http://localhost:8000/api/notes/1 в своем браузере:

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

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

Обновление и удаление открытки

На этот раз вы начнете с создания функций в notes.py, прежде чем создавать операции в swagger.yml.

Добавьте update() и delete() в notes.py:

# notes.py  # ... def update(note_id, note):     existing_note = Note.query.get(note_id)      if existing_note:         update_note = note_schema.load(note, session=db.session)         existing_note.content = update_note.content         db.session.merge(existing_note)         db.session.commit()         return note_schema.dump(existing_note), 201     else:         abort(404, f"Note with ID {note_id} not found")  def delete(note_id):     existing_note = Note.query.get(note_id)      if existing_note:         db.session.delete(existing_note)         db.session.commit()         return make_response(f"{note_id} successfully deleted", 204)     else:         abort(404, f"Note with ID {note_id} not found")

Если сравнить update() и delete(), то они имеют схожую структуру. Обе функции ищут существующую открытку и работают с сессией базы данных.

Для работы update() вы также принимаете объект note в качестве аргумента, который содержит атрибут .content, который вы можете обновить.

Напротив, вам нужно знать только ID заметки, от которой вы хотите избавиться при вызове delete().

Затем создайте две операции в swagger.yml, которые ссылаются на notes.update и notes.delete:

# swagger.yml  # ... paths:   /people:     # ...   /people/{lname}:     # ...   /notes/{note_id}:     get:       # ...     put:       tags:         - Notes       operationId: "notes.update"       summary: "Update a note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "200":           description: "Successfully updated note"       requestBody:         x-body-name: "note"         content:           application/json:             schema:               type: "object"               properties:                 content:                   type: "string"     delete:       tags:         - Notes       operationId: "notes.delete"       summary: "Delete a note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "204":           description: "Successfully deleted note"

Опять же, структуры put и delete похожи. Главное отличие в том, что вам нужно предоставить requestBody, который содержит данные открытки для обновления объекта базы данных.

Теперь вы создали конечные точки для работы с существующими открытками. Далее вы добавите конечную точку для создания открытки.

Создание открытки для персонажа

До сих пор вы могли читать, обновлять и удалять одну открытку. Это действия, которые вы можете выполнять с существующими открытками. Теперь пришло время добавить функциональность в ваш REST API, чтобы также создавать новую открытку. Добавьте create() в notes.py:

# notes.py  from flask import make_response, abort  from config import db from models import Note, Person, note_schema # ... def create(note):     person_id = note.get("person_id")     person = Person.query.get(person_id)      if person:         new_note = note_schema.load(note, session=db.session)         person.notes.append(new_note)         db.session.commit()         return note_schema.dump(new_note), 201     else:         abort(             404, f"Person not found for ID: {person_id}"         )

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

Сначала вы ищете владельца открытки, используя person_id, который вы предоставляете с аргументом notes для create(). Если этот персонаж существует в базе данных, то вы переходите к добавлению новой открытки в person.notes.

Хотя в этом случае вы работаете с таблицей базы данных person, SQLAlchemy позаботится о том, чтобы открытка была добавлена в таблицу открыток.

Чтобы получить доступ к notes.create с помощью вашего API, перейдите в swagger.yml и добавьте еще одну конечную точку:

# swagger.yml  # ... paths:   /people:     # ...   /people/{lname}:     # ...   /notes:     post:       operationId: "notes.create"       tags:         - Notes       summary: "Create a note associated with a person"       requestBody:           description: "Note to create"           required: True           x-body-name: "note"           content:             application/json:               schema:                 type: "object"                 properties:                   person_id:                     type: "integer"                   content:                     type: "string"       responses:         "201":           description: "Successfully created a note"   /notes/{note_id}:     # ...

Вы добавляете конечную точку /notes прямо перед конечной точкой /notes/{noted_id}. Таким образом, вы упорядочиваете конечные точки открыток от общих к частным. Этот порядок помогает вам ориентироваться в файле swagger.yml, когда ваш API разрастается.

С данными в блоке схемы вы предоставляете Marshmallow информацию о том, как сериализовать открытку в вашем API. Если вы сравните эту схему данных Note с моделью Note в models.py, то заметите, что имена person_id и content совпадают. То же самое касается типов полей.

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

После того, как все конечные точки для обработки ваших открыток добавлены, пора взглянуть на аннотации API.

Аннотации API

С внесением вышеуказанных изменений вы можете использовать свой API для добавления, обновления и удаления открыток. Посетите свой веб-интерфейс Swagger по адресу http://localhost:8000/api/ui и убедитесь, что ваши конечные точки API Flask REST работают! Любые изменения, которые вы вносите с помощью своего API, также отображаются на вашем веб-интерфейсе.

Заключение

В этой статье вы настроили свою базу данных SQLite для реализации связей. После этого вы перевели словарь PEOPLE_NOTES в данные, соответствующие структуре вашей базы данных, и превратили свой API Flask REST в веб-приложение для хранения заметок.

В этой третьей части серии вы узнали, как:
— Работать с несколькими таблицами с взаимосвязанной информацией в базе данных
— Создавать связи «один ко многим» в базе данных
— Управлять связями с помощью SQLAlchemy
— Сериализовать сложные схемы данных со связями с помощью Marshmallow
— Отображать связанные объекты в клиентском интерфейсе

Понимание того, как создавать и использовать связи данных в базе данных, дает вам мощный инструмент для решения многих сложных проблем. Помимо примера «один ко многим» из этого руководства существуют и другие связи. Другими распространенными являются «один к одному», «многие ко многим» и «многие к одному». Все они занимают свое место в вашем арсенале, и SQLAlchemy поможет вам справиться с ними всеми!

Вы успешно создали API REST для отслеживания открыток персонажей, которые могут посетить вас в течение года. В вашей базе данных есть такие персонажи, как Зубная фея, Пасхальный кролик и Кнехт Рупрехт. Добавляя открытки, вы можете отслеживать свои добрые дела и, возможно, получать от них ценные подарки.

Полный исходный код

Структура файлов и папок

rp_flask_api/ ├── templates/ │   └── home.html ├── venv/ # виртуальное окружение ├── app.py ├── config.py ├── create_db.py # файл создания и первоначального заполнения базы данных ├── models.py ├── notes.py ├── people.db # файл базы данных ├── people.py ├── requirements.txt # файл с указанием зависимостей, установка через pip install -r requirements.txt └── swagger.yml

templates/home.html

<!-- templates/home.html -->  <!DOCTYPE html> <html lang="en"> <head>     <meta charset="UTF-8">     <title>RP Flask REST API</title> </head> <body>     <h1>Hello, People!</h1>     {% for person in people %}     <h2>{{ person.fname }} {{ person.lname }}</h2>     <ul>         {% for note in person.notes %}         <li>{{ note.content }}</li>         {% endfor %}     </ul>     {% endfor %} </body> </html>

app.py

# app.py  import config from flask import render_template from models import Person  app = config.connex_app app.add_api(config.basedir / "swagger.yml")  @app.route("/") def home():     people = Person.query.all()     return render_template("home.html", people=people)  if __name__ == "__main__":     app.run(host="0.0.0.0", port=8000)

config.py

# config.py  import pathlib import connexion from flask_marshmallow import Marshmallow from flask_sqlalchemy import SQLAlchemy  basedir = pathlib.Path(__file__).parent.resolve() connex_app = connexion.App(__name__, specification_dir=basedir)  app = connex_app.app app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{basedir / 'people.db'}" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False  db = SQLAlchemy(app) ma = Marshmallow(app)

create_db.py

# create_db.py  from datetime import datetime from config import app, db from models import Person, Note  PEOPLE_NOTES = [     {         "lname": "Fairy",         "fname": "Tooth",         "notes": [             ("I brush my teeth after each meal.", "2022-01-06 17:10:24"),             ("The other day a friend said, I have big teeth.", "2022-03-05 22:17:54"),             ("Do you pay per gram?", "2022-03-05 22:18:10"),         ],     },     {         "lname": "Ruprecht",         "fname": "Knecht",         "notes": [             ("I swear, I'll do better this year.", "2022-01-01 09:15:03"),             ("Really! Only good deeds from now on!", "2022-02-06 13:09:21"),         ],     },     {         "lname": "Bunny",         "fname": "Easter",         "notes": [             ("Please keep the current inflation rate in mind!", "2022-01-07 22:47:54"),             ("No need to hide the eggs this time.", "2022-04-06 13:03:17"),         ],     }, ]  with app.app_context():     db.drop_all()     db.create_all()     for data in PEOPLE_NOTES:         new_person = Person(lname=data.get("lname"), fname=data.get("fname"))         for content, timestamp in data.get("notes", []):             new_person.notes.append(                 Note(                     content=content,                     timestamp=datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S"),                 )             )         db.session.add(new_person)     db.session.commit()

models.py

# models.py  from datetime import datetime from marshmallow_sqlalchemy import fields from config import db, ma  class Note(db.Model):     __tablename__ = "note"     id = db.Column(db.Integer, primary_key=True)     person_id = db.Column(db.Integer, db.ForeignKey("person.id"))     content = db.Column(db.String, nullable=False)     timestamp = db.Column(         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow     )  class NoteSchema(ma.SQLAlchemyAutoSchema):     class Meta:         model = Note         load_instance = True         sqla_session = db.session         include_fk = True  class Person(db.Model):     __tablename__ = "person"     id = db.Column(db.Integer, primary_key=True)     lname = db.Column(db.String(32), unique=True)     fname = db.Column(db.String(32))     timestamp = db.Column(         db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow     )     notes = db.relationship(         Note,         backref="person",         cascade="all, delete, delete-orphan",         single_parent=True,         order_by="desc(Note.timestamp)"     )  class PersonSchema(ma.SQLAlchemyAutoSchema):     class Meta:         model = Person         load_instance = True         sqla_session = db.session         include_relationships = True     notes = fields.Nested(NoteSchema, many=True)  note_schema = NoteSchema() person_schema = PersonSchema() people_schema = PersonSchema(many=True)

nodes.py

# notes.py  from flask import abort, make_response  from config import db from models import Note, Person, note_schema  def read_one(note_id):     note = Note.query.get(note_id)      if note is not None:         return note_schema.dump(note)     else:         abort(             404, f"Note with ID {note_id} not found"         )  def create(note):     person_id = note.get("person_id")     person = Person.query.get(person_id)      if person:         new_note = note_schema.load(note, session=db.session)         person.notes.append(new_note)         db.session.commit()         return note_schema.dump(new_note), 201     else:         abort(             404, f"Person not found for ID: {person_id}"         )  def update(note_id, note):     existing_note = Note.query.get(note_id)      if existing_note:         update_note = note_schema.load(note, session=db.session)         existing_note.content = update_note.content         db.session.merge(existing_note)         db.session.commit()         return note_schema.dump(existing_note), 201     else:         abort(404, f"Note with ID {note_id} not found")  def delete(note_id):     existing_note = Note.query.get(note_id)      if existing_note:         db.session.delete(existing_note)         db.session.commit()         return make_response(f"{note_id} successfully deleted", 204)     else:         abort(404, f"Note with ID {note_id} not found")

people.py

# people.py  from config import db from flask import abort, make_response from models import Person, people_schema, person_schema  def read_all():     people = Person.query.all()     return people_schema.dump(people)  def create(person):     lname = person.get("lname")     existing_person = Person.query.filter(Person.lname == lname).one_or_none()      if existing_person is None:         new_person = person_schema.load(person, session=db.session)         db.session.add(new_person)         db.session.commit()         return person_schema.dump(new_person), 201     else:         abort(406, f"Person with last name {lname} already exists")  def read_one(lname):     person = Person.query.filter(Person.lname == lname).one_or_none()      if person is not None:         return person_schema.dump(person)     else:         abort(404, f"Person with last name {lname} not found")  def update(lname, person):     existing_person = Person.query.filter(Person.lname == lname).one_or_none()      if existing_person:         update_person = person_schema.load(person, session=db.session)         existing_person.fname = update_person.fname         db.session.merge(existing_person)         db.session.commit()         return person_schema.dump(existing_person), 201     else:         abort(404, f"Person with last name {lname} not found")  def delete(lname):     existing_person = Person.query.filter(Person.lname == lname).one_or_none()      if existing_person:         db.session.delete(existing_person)         db.session.commit()         return make_response(f"{lname} successfully deleted", 200)     else:         abort(404, f"Person with last name {lname} not found")

requirements.txt

# requirements.txt  connexion[Flask]==3.1.0 connexion[uvicorn]==3.1.0 flask-marshmallow==1.2.1 Flask-SQLAlchemy==3.1.1 marshmallow-sqlalchemy==1.1.0 swagger_ui_bundle==1.1.0

swagger.yml

# swagger.yml  openapi: 3.0.0 info:   title: "RP Flask REST API"   description: "An API about people and notes"   version: "1.0.0"  servers:   - url: "/api"  components:   schemas:     Person:       type: "object"       required:         - lname       properties:         fname:           type: "string"         lname:           type: "string"   parameters:     lname:       name: "lname"       description: "Last name of the person to get"       in: path       required: True       schema:         type: "string"     note_id:       name: "note_id"       description: "ID of the note"       in: path       required: true       schema:         type: "integer"  paths:   /people:     get:       operationId: "people.read_all"       tags:         - "People"       summary: "Read the list of people"       responses:         "200":           description: "Successfully read people list"     post:       operationId: "people.create"       tags:         - People       summary: "Create a person"       requestBody:         x-body-name: "person"         description: "Person to create"         required: True         content:           application/json:             schema:               $ref: "#/components/schemas/Person"       responses:         "201":           description: "Successfully created person"   /people/{lname}:     get:       operationId: "people.read_one"       tags:         - People       summary: "Read one person"       parameters:         - $ref: "#/components/parameters/lname"       responses:         "200":           description: "Successfully read person"     put:       tags:         - People       operationId: "people.update"       summary: "Update a person"       parameters:         - $ref: "#/components/parameters/lname"       responses:         "200":           description: "Successfully updated person"       requestBody:         x-body-name: "person"         content:           application/json:             schema:               $ref: "#/components/schemas/Person"     delete:       tags:         - People       operationId: "people.delete"       summary: "Delete a person"       parameters:         - $ref: "#/components/parameters/lname"       responses:         "204":           description: "Successfully deleted person"   /notes:     post:       operationId: "notes.create"       tags:         - Notes       summary: "Create a note associated with a person"       requestBody:           description: "Note to create"           required: True           x-body-name: "note"           content:             application/json:               schema:                 type: "object"                 properties:                   person_id:                     type: "integer"                   content:                     type: "string"       responses:         "201":           description: "Successfully created a note"   /notes/{note_id}:     get:       operationId: "notes.read_one"       tags:         - Notes       summary: "Read one note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "200":           description: "Successfully read one note"     put:       tags:         - Notes       operationId: "notes.update"       summary: "Update a note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "200":           description: "Successfully updated note"       requestBody:         x-body-name: "note"         content:           application/json:             schema:               type: "object"               properties:                 content:                   type: "string"     delete:       tags:         - Notes       operationId: "notes.delete"       summary: "Delete a note"       parameters:         - $ref: "#/components/parameters/note_id"       responses:         "204":           description: "Successfully deleted note"


ссылка на оригинал статьи https://habr.com/ru/articles/860964/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *