A Comprehensive Example#

Flask API Server Code#

app.py
  1# -*- coding: utf-8 -*-
  2
  3# standard library
  4import enum
  5from pathlib import Path
  6from datetime import datetime
  7
  8# third-party
  9import flask
 10import flask_restless
 11import flask_sqlalchemy
 12import sqlalchemy.orm as orm
 13
 14# Create the Flask application and the Flask-SQLAlchemy object.
 15app = flask.Flask(__name__)
 16# Tell flask-sqlalchemy how to connect to the database
 17path_db = Path(__file__).absolute().parent / "app.sqlite"
 18app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{path_db}"
 19# Create the virtual database object (it's a flask-sqlalchemy concepts).
 20db = flask_sqlalchemy.SQLAlchemy(app)
 21
 22
 23# Declare data models
 24#
 25# - User: a YouTube user
 26# - Video: a video uploaded by user, one video can have only one user
 27# - Playlist: a playlist created by user, it could have many videos.
 28# - User / Video authorship is one-to-many relationship
 29# - User / Playlist ownership is one-to-many relationship
 30# - Playlist / Video is many-to-many relationship
 31class User(db.Model):
 32    __tablename__ = "user"
 33
 34    id: orm.Mapped[str] = db.Column(db.Unicode, primary_key=True)
 35    name: orm.Mapped[str] = db.Column(db.Unicode)
 36    create_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 37    update_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 38    deleted: orm.Mapped[int] = db.Column(db.Integer)
 39
 40
 41# for many-to-many relationship, define an association table
 42# and then pick one of the involved entity model, set a relationship there
 43# in this example, we will set a relationship in Playlist at videos attribute
 44playlist_video = db.Table(
 45    # table name doesn't matter
 46    "playlist_video",
 47    db.Column(
 48        # column name doesn't matter
 49        "playlist_id",
 50        db.Unicode,
 51        # foreign key does matter, it should be ${table_name}.${primary_key_name}
 52        db.ForeignKey("playlist.id"),
 53    ),
 54    db.Column(
 55        # column name doesn't matter
 56        "video_id",
 57        db.Unicode,
 58        # foreign key does matter, it should be ${table_name}.${primary_key_name}
 59        db.ForeignKey("video.id"),
 60    ),
 61)
 62
 63
 64# for one-to-many relationship, define a relationship in the 'many' entity.
 65# in this example, one user can have many videos, then the 'many' entity is the Video,
 66# so we will set a relationship in 'Video' at 'author' attribute
 67class Video(db.Model):
 68    __tablename__ = "video"
 69
 70    id: orm.Mapped[str] = db.Column(db.Unicode, primary_key=True)
 71    title: orm.Mapped[str] = db.Column(db.Unicode)
 72    create_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 73    update_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 74    deleted: orm.Mapped[int] = db.Column(db.Integer)
 75    author_id: orm.Mapped[str] = db.Column(db.Unicode, db.ForeignKey("user.id"))
 76
 77    # This is not a column, it is a relationship
 78    author: orm.Mapped[User] = db.relationship(
 79        User,
 80        # backref will generate a new attribute (not column) in User model
 81        # you can use this attribute to get all videos uploaded by this user.
 82        # note that you have to declare this backref attribute in User model
 83        # with original sqlalchemy.orm
 84        # (see example: https://github.com/MacHu-GWU/learn_sqlalchemy-project/blob/master/docs/source/02-orm/02-relationship-configuration/02-one-to-many/e1_simple_one_to_many.py#L23)
 85        # but you don't do (you cannot) this and flask-sqlalchemy will do this for you
 86        backref=db.backref("owned_videos"),
 87    )
 88
 89
 90class Playlist(db.Model):
 91    __tablename__ = "playlist"
 92
 93    id: orm.Mapped[str] = db.Column(db.Unicode, primary_key=True)
 94    title: orm.Mapped[str] = db.Column(db.Unicode)
 95    create_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 96    update_at: orm.Mapped[datetime] = db.Column(db.DateTime)
 97    deleted: orm.Mapped[int] = db.Column(db.Integer)
 98    owner_id: orm.Mapped[str] = db.Column(db.Unicode, db.ForeignKey("user.id"))
 99
100    # this one is similar to Video.author
101    owner: orm.Mapped[User] = db.relationship(
102        User, backref=db.backref("owned_playlists")
103    )
104    # backref will generate a new attribute (not column) in Video model
105    # you can use this attribute to get all playlists that include this video.
106    # note that you have to declare this backref attribute in Video model
107    # with original sqlalchemy.orm
108    # (see example: https://github.com/MacHu-GWU/learn_sqlalchemy-project/blob/master/docs/source/02-orm/02-relationship-configuration/03-many-to-many/e1_many_to_many.py#L51)
109    # but you don't do (you cannot) this and flask-sqlalchemy will do this for you
110    videos = db.relationship(
111        Video,
112        # for many-to-many, use secondary to specify the association table
113        secondary=playlist_video,
114        backref="playlists",
115    )
116
117
118# Define an enumeration for the type of entity.
119class EntityTypeEnum(str, enum.Enum):
120    user = "user"
121    video = "video"
122    playlist = "playlist"
123
124
125ETE = EntityTypeEnum
126
127
128# Create the database tables.
129with app.app_context():
130    db.create_all()
131
132    # Create the Flask-Restless API manager.
133    manager = flask_restless.APIManager(app, session=db.session)
134
135    # Create API endpoints, which will be available at /api/<tablename> by
136    # default. Allowed HTTP methods can be specified as well.
137    manager.create_api(
138        User,
139        methods=["GET", "POST", "PATCH", "DELETE"],
140        # if you use auto-increment integer primary key, you should set allow_client_generated_ids=False
141        # if you use string primary key, you can set allow_client_generated_ids=True
142        allow_client_generated_ids=True,
143    )
144    manager.create_api(
145        Video,
146        # Get = retrieve
147        # Post = create
148        # Patch = update
149        # Delete = delete
150        methods=["GET", "POST", "PATCH", "DELETE"],
151        allow_client_generated_ids=True,
152        # if you want to replace many videos owned by a user
153        # or many videos in a playlist in one API call, you should set allow_to_many_replacement=True
154        allow_to_many_replacement=True,
155    )
156    manager.create_api(
157        Playlist,
158        methods=["GET", "POST", "PATCH", "DELETE"],
159        allow_client_generated_ids=True,
160        allow_to_many_replacement=True,
161    )

Import Dependencies and ORM Model#

[1]:
import typing as T
import json
import uuid
import requests
from datetime import datetime

import sqlalchemy as sa
import sqlalchemy.orm as orm

from rich import print as rprint
from rich.console import Console
from rich.panel import Panel

from app import path_db, User, Video, Playlist, playlist_video, ETE

Define Some Variables and Helper Functions#

[2]:
engine = sa.create_engine(f"sqlite:///{path_db}")

host = "http://127.0.0.1:5000"
post_headers = {
    "Content-Type": "application/vnd.api+json",
    "Accept": "application/vnd.api+json",
}
get_headers = {
    "Accept": "application/vnd.api+json",
}
console = Console()


def get_utc_now():
    return datetime.utcnow()


def clear_data():
    with engine.connect() as conn:
        conn.execute(Video.__table__.delete())
        conn.execute(User.__table__.delete())
        conn.execute(Playlist.__table__.delete())
        conn.execute(playlist_video.delete())
        conn.commit()


def make_request(meth: T.Callable, kwargs: T.Dict[str, T.Any]) -> requests.Response:
    """
    A helper function that make HTTP request, and automatically print request and response.
    """
    console.rule("API Request")
    rprint(kwargs)

    response: requests.Response = meth(**kwargs)
    console.rule("API Response status code")
    rprint(f"{response.status_code = }")
    console.rule("API Response headers")
    rprint(dict(response.headers))
    console.rule("API Response data")
    if response.status_code in [200, 201]:
        rprint(json.loads(response.text))
    elif response.status_code == 204:
        print("No content")
    else:
        rprint(response.text)
    return response

Clear Data for a Fresh Start#

[3]:
clear_data()

Create User#

[4]:
rprint(Panel("Create a user"))
user_id_1 = str(uuid.uuid4())
user_id_1_create_at = get_utc_now()

url = f"{host}/api/{ETE.user.value}"
data = {
    "data": {
        "type": ETE.user.value,
        "id": user_id_1,
        "attributes": {
            "name": "Alice",
            "create_at": user_id_1_create_at.isoformat(),
            "update_at": user_id_1_create_at.isoformat(),
            "deleted": 0,
        },
    },
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.post, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    user = ses.get(User, user_id_1)
    assert user.id == user_id_1
    assert user.name == "Alice"
    assert user.create_at == user_id_1_create_at
    assert user.update_at == user_id_1_create_at
    assert user.deleted == 0
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Create a user                                                                                                   │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/user',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': {
            'type': 'user',
            'id': '62608ecc-5809-4534-a97e-246185f8e876',
            'attributes': {
                'name': 'Alice',
                'create_at': '2024-06-02T04:44:04.906714',
                'update_at': '2024-06-02T04:44:04.906714',
                'deleted': 0
            }
        }
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 201
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:44:04 GMT',
    'Location': 'http://127.0.0.1:5000/api/user/62608ecc-5809-4534-a97e-246185f8e876',
    'Content-Type': 'application/vnd.api+json',
    'Content-Length': '318',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
{
    'data': {
        'attributes': {
            'create_at': '2024-06-02T04:44:04.906714',
            'deleted': 0,
            'name': 'Alice',
            'update_at': '2024-06-02T04:44:04.906714'
        },
        'id': '62608ecc-5809-4534-a97e-246185f8e876',
        'relationships': {'owned_playlists': {'data': []}, 'owned_videos': {'data': []}},
        'type': 'user'
    },
    'jsonapi': {'version': '1.0'}
}

Get user 1#

[5]:
rprint(Panel("Get user 1"))
url = f"{host}/api/{ETE.user.value}/{user_id_1}"
kwargs = dict(
    url=url,
    headers=get_headers,
)
response = make_request(requests.get, kwargs)
data = json.loads(response.text)
assert data["data"]["id"] == user_id_1
assert data["data"]["attributes"]["name"] == "Alice"
assert data["data"]["attributes"]["deleted"] == 0
assert data["data"]["attributes"]["create_at"] == user_id_1_create_at.isoformat()
assert data["data"]["attributes"]["update_at"] == user_id_1_create_at.isoformat()
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Get user 1                                                                                                      │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/user/62608ecc-5809-4534-a97e-246185f8e876',
    'headers': {'Accept': 'application/vnd.api+json'}
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 200
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:44:30 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Content-Length': '318',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
{
    'data': {
        'attributes': {
            'create_at': '2024-06-02T04:44:04.906714',
            'deleted': 0,
            'name': 'Alice',
            'update_at': '2024-06-02T04:44:04.906714'
        },
        'id': '62608ecc-5809-4534-a97e-246185f8e876',
        'relationships': {'owned_playlists': {'data': []}, 'owned_videos': {'data': []}},
        'type': 'user'
    },
    'jsonapi': {'version': '1.0'}
}

Update the user 1#

[6]:
rprint(Panel("Update the user 1"))
url = f"{host}/api/{ETE.user.value}/{user_id_1}"

user_id_1_update_at = get_utc_now()
data = {
    "data": {
        "type": ETE.user.value,
        "id": user_id_1,
        "attributes": {
            "name": "Bob",
            "update_at": user_id_1_update_at.isoformat(),
        },
    },
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.patch, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    user = ses.get(User, user_id_1)
    assert user.id == user_id_1
    assert user.name == "Bob"
    assert user.create_at == user_id_1_create_at
    assert user.update_at == user_id_1_update_at
    assert user.deleted == 0
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Update the user 1                                                                                               │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/user/62608ecc-5809-4534-a97e-246185f8e876',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': {
            'type': 'user',
            'id': '62608ecc-5809-4534-a97e-246185f8e876',
            'attributes': {'name': 'Bob', 'update_at': '2024-06-02T04:44:52.987591'}
        }
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 204
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:44:53 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
No content

Create a video#

[7]:
rprint(Panel("Create a video"))
video_id_1 = str(uuid.uuid4())
video_id_1_create_at = get_utc_now()

url = f"{host}/api/{ETE.video.value}"
data = {
    "data": {
        "type": ETE.video.value,
        "id": video_id_1,
        "attributes": {
            "title": "user 1 video 1",
            "create_at": video_id_1_create_at.isoformat(),
            "update_at": video_id_1_create_at.isoformat(),
            "deleted": 0,
            "author_id": user_id_1,
        },
    },
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.post, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    video = ses.get(Video, video_id_1)
    assert video.id == video_id_1
    assert video.title == "user 1 video 1"
    assert video.create_at == video_id_1_create_at
    assert video.update_at == video_id_1_create_at
    assert video.deleted == 0
    assert video.author_id == user_id_1
    assert video.author.id == user_id_1
    assert video.author.name == "Bob"

    user = ses.get(User, user_id_1)
    owned_videos = user.owned_videos
    assert len(owned_videos) == 1
    video = owned_videos[0]
    assert video.id == video_id_1
    assert video.title == "user 1 video 1"
    assert video.create_at == video_id_1_create_at
    assert video.update_at == video_id_1_create_at
    assert video.deleted == 0
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Create a video                                                                                                  │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/video',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': {
            'type': 'video',
            'id': 'ca585605-67d8-4e77-becc-8afa09a2fd82',
            'attributes': {
                'title': 'user 1 video 1',
                'create_at': '2024-06-02T04:45:05.647971',
                'update_at': '2024-06-02T04:45:05.647971',
                'deleted': 0,
                'author_id': '62608ecc-5809-4534-a97e-246185f8e876'
            }
        }
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 201
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:45:05 GMT',
    'Location': 'http://127.0.0.1:5000/api/video/ca585605-67d8-4e77-becc-8afa09a2fd82',
    'Content-Type': 'application/vnd.api+json',
    'Content-Length': '377',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
{
    'data': {
        'attributes': {
            'create_at': '2024-06-02T04:45:05.647971',
            'deleted': 0,
            'title': 'user 1 video 1',
            'update_at': '2024-06-02T04:45:05.647971'
        },
        'id': 'ca585605-67d8-4e77-becc-8afa09a2fd82',
        'relationships': {
            'author': {'data': {'id': '62608ecc-5809-4534-a97e-246185f8e876', 'type': 'user'}},
            'playlists': {'data': []}
        },
        'type': 'video'
    },
    'jsonapi': {'version': '1.0'}
}

Update video ownership relationship#

[8]:
rprint(Panel("Update video ownership relationship"))
# create a new user, so we can switch the ownership to this new user
user_id_2 = str(uuid.uuid4())
user_id_2_create_at = get_utc_now()

url = f"{host}/api/{ETE.user.value}"
data = {
    "data": {
        "type": ETE.user.value,
        "id": user_id_2,
        "attributes": {
            "name": "Bella",
            "create_at": user_id_2_create_at.isoformat(),
            "update_at": user_id_2_create_at.isoformat(),
            "deleted": 0,
        },
    },
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
requests.post(**kwargs)

# update video ownership
url = f"{host}/api/{ETE.video.value}/{video_id_1}/relationships/author"
data = {
    "data": {
        "type": ETE.user.value,
        "id": user_id_2,
    },
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.patch, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    video = ses.get(Video, video_id_1)
    assert video.author_id == user_id_2
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Update video ownership relationship                                                                             │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/video/ca585605-67d8-4e77-becc-8afa09a2fd82/relationships/author',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {'data': {'type': 'user', 'id': '3d8897cd-af7f-4739-9921-069844277f5d'}}
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 204
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:45:21 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
No content

Create playlist video relationship#

[9]:
rprint(Panel("Create playlist video relationship"))
# first, create two more videos and two playlists
# now we have 3 videos and 2 playlists
video_id_2 = str(uuid.uuid4())
video_id_3 = str(uuid.uuid4())
playlist_id_1 = str(uuid.uuid4())
playlist_id_2 = str(uuid.uuid4())
utc_now = get_utc_now()

url = f"{host}/api/{ETE.video.value}"
for ith, video_id in enumerate([video_id_2, video_id_3], start=2):
    data = {
        "data": {
            "type": ETE.video.value,
            "id": video_id,
            "attributes": {
                "title": f"user 1 video {ith}",
                "create_at": utc_now.isoformat(),
                "update_at": utc_now.isoformat(),
                "deleted": 0,
                "author_id": user_id_1,
            },
        },
    }
    kwargs = dict(
        url=url,
        headers=post_headers,
        json=data,
    )
    requests.post(**kwargs)

url = f"{host}/api/{ETE.playlist.value}"
for ith, playlist_id in enumerate([playlist_id_1, playlist_id_2], start=1):
    data = {
        "data": {
            "type": ETE.playlist.value,
            "id": playlist_id,
            "attributes": {
                "title": f"user 1 playlist {ith}",
                "create_at": utc_now.isoformat(),
                "update_at": utc_now.isoformat(),
                "deleted": 0,
                "owner_id": user_id_1,
            },
        },
    }
    kwargs = dict(
        url=url,
        headers=post_headers,
        json=data,
    )
    requests.post(**kwargs)

# playlist 1 includes video 1 and video 2
# playlist 2 includes video 2 and video 3
url = f"{host}/api/{ETE.playlist.value}/{playlist_id_1}/relationships/videos"
data = {
    "data": [
        {
            "type": ETE.video.value,
            "id": video_id_1,
        },
        {
            "type": ETE.video.value,
            "id": video_id_2,
        },
    ],
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.patch, kwargs)

url = f"{host}/api/{ETE.playlist.value}/{playlist_id_2}/relationships/videos"
data = {
    "data": [
        {
            "type": ETE.video.value,
            "id": video_id_2,
        },
        {
            "type": ETE.video.value,
            "id": video_id_3,
        },
    ],
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.patch, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    playlist = ses.get(Playlist, playlist_id_1)
    assert {video.id for video in playlist.videos} == {
        video_id_1,
        video_id_2,
    }
    video = ses.get(Video, video_id_2)
    assert {playlist.id for playlist in video.playlists} == {
        playlist_id_1,
        playlist_id_2,
    }
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Create playlist video relationship                                                                              │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/playlist/3e42c410-bf51-4fa2-ab3d-fd19d8ac327a/relationships/videos',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': [
            {'type': 'video', 'id': 'ca585605-67d8-4e77-becc-8afa09a2fd82'},
            {'type': 'video', 'id': '73cc46c0-5aff-47f4-bc1b-4a5a9d816164'}
        ]
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 204
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:45:38 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
No content
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/playlist/18ab3ee2-8f44-45b7-8add-ed4687e43aca/relationships/videos',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': [
            {'type': 'video', 'id': '73cc46c0-5aff-47f4-bc1b-4a5a9d816164'},
            {'type': 'video', 'id': '4e02748a-21a9-45d7-a0b6-ba41aedfd944'}
        ]
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 204
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:45:38 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
No content

Update playlist video relationship#

[10]:
rprint(Panel("Update playlist video relationship"))

url = f"{host}/api/{ETE.playlist.value}/{playlist_id_1}/relationships/videos"
data = {
    "data": [
        {
            "type": ETE.video.value,
            "id": video_id_1,
        },
        {
            "type": ETE.video.value,
            "id": video_id_3,
        },
    ],
}
kwargs = dict(
    url=url,
    headers=post_headers,
    json=data,
)
response = make_request(requests.patch, kwargs)

# verify the data in the database
with orm.Session(engine) as ses:
    playlist = ses.get(Playlist, playlist_id_1)
    assert {video.id for video in playlist.videos} == {
        video_id_1,
        video_id_3,
    }

╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Update playlist video relationship                                                                              │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
─────────────────────────────────────────────────── API Request ───────────────────────────────────────────────────
{
    'url': 'http://127.0.0.1:5000/api/playlist/3e42c410-bf51-4fa2-ab3d-fd19d8ac327a/relationships/videos',
    'headers': {'Content-Type': 'application/vnd.api+json', 'Accept': 'application/vnd.api+json'},
    'json': {
        'data': [
            {'type': 'video', 'id': 'ca585605-67d8-4e77-becc-8afa09a2fd82'},
            {'type': 'video', 'id': '4e02748a-21a9-45d7-a0b6-ba41aedfd944'}
        ]
    }
}
──────────────────────────────────────────── API Response status code ─────────────────────────────────────────────
response.status_code = 204
────────────────────────────────────────────── API Response headers ───────────────────────────────────────────────
{
    'Server': 'Werkzeug/3.0.3 Python/3.8.13',
    'Date': 'Sun, 02 Jun 2024 04:45:49 GMT',
    'Content-Type': 'application/vnd.api+json',
    'Connection': 'close'
}
──────────────────────────────────────────────── API Response data ────────────────────────────────────────────────
No content
[ ]: