A collection of computer, gaming and general nerdy things.

Showing posts with label flask-restful. Show all posts
Showing posts with label flask-restful. Show all posts

Tuesday, September 2, 2014

APIs V2: SQLAlchemy Boogaloo

Now that we have our skeleton API built and we can pull the metadata out of MP3 files, we can build an actual data driven API instead of processing everything by hand.

Tools needed

  • Flask
  • SQLAlchemy
  • MutagenX (Or Mutagen if you're running Python2)
  • Flask-Restful
  • Flask-Marshmallow
  • Flask-Script (working on replacing this with Click)
Why no TinyTag? I let TinyTag go after figuring out that the encoding error is centered there. Originally, I had not used Mutagen because it had given me issues. Turns out Mutagen supports Python2. There's a fork -- MutagenX -- that supports Python3. Problem solved.

Setup

Setting up the data driven API is pretty straight forward. However, I'm going to touch on a few more advanced things in here because simply doing an incremental development blog post would be terribly boring to me. The first slightly advanced thing is going to be modularizing the Flask app. Here's what my directory layout looks like right now:
FlaskAmp/
├── app
│   ├── config.py
│   ├── dev-sqlite.db
│   ├── factory.py
│   ├── __init__.py
│   ├── models.py
│   ├── serializers.py
│   └── utils.py
├── LICENSE
├── manager.py
├── README.md
└── requirements
It's a pretty standard layout. But it bears repeating. Under the app directory is where our action takes place. We'll go over each of these files.
In []:
#app/models.py
from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class Artist(db.Model):
    __tablename__ = "artists"
    id = db.Column('id', db.Integer, primary_key=True)
    name = db.Column('name', db.Unicode, unique=True)
    albums = db.relationship('Album', backref=db.backref('artist', uselist=False), order_by='Album.name')
    tracks = db.relationship('Track', backref=db.backref('artist', uselist=False), lazy='noload')

class Album(db.Model):
    __tablename__ = 'albums'
    id = db.Column('id', db.Integer, primary_key=True)
    name = db.Column('name', db.Unicode, unique=True)
    artist_id = db.Column('artist_id', db.Integer, db.ForeignKey('artists.id'))
    tracks = db.relationship(
        'Track', backref=db.backref('album', uselist=False),
        lazy='immediate', order_by='Track.position')
    
class Track(db.Model):
    __tablename__ = 'tracks'
    id = db.Column('id', db.Integer, primary_key=True)
    name = db.Column('name', db.Unicode, primary_key=True)
    length = db.Column('length', db.Integer)
    position = db.Column('position', db.Integer)
    location = db.Column('location', db.Unicode)
    stream = db.Column('stream', db.String)
    artist_id = db.Column('artist_id', db.Integer, db.ForeignKey('artists.id'))
    album_id = db.Column('album_id', db.Integer, db.ForeignKey('albums.id'))
This is a pretty standard setup for Flask-SQLAlchemy models. The only thing most people might not be familar with is the lazy='noload' on the relationship from Artist to Track. This tell SQLAlchemy to never load this data. Why? For now, we're not interested in loading it. Given that I'm already working on a much more indepth model structure, this'll get changed in the future.
In []:
#app/serializers.py
#this is pretty similar to before
from flask.ext.marshmallow import Marshmallow

ma = Marshmallow()

class ArtistSerializer(ma.Serializer):
    albums = ma.Nested('AlbumSerializer', exclude=('artist',), many=True)
    
    _links = ma.Hyperlinks({
        'self':ma.URL('artist', id='<id>'),
        'collection':ma.URL('artist')
        })
    
    class Meta:
        fields = ('id', 'name', 'albums', 'links')

class AlbumSerializer(ma.Serializer):
    artist = ma.Nested('ArtistSerializer', exclude=('albums',))
    tracks = ma.Nested('TrackSerializer', exclude=('albums', 'artist'), many=True)
    
    _links = ma.Hyperlinks({
        'self':ma.URL('album', id='<id>'),
        'collection':ma.URL('album')
        })
    
    class Meta:
        fields = ('id', 'name', 'artist', 'tracks', 'links')

class TrackSerializer(ma.Serializer):
        albums = ma.Nested('AlbumSerializer', exclude=('artist', 'tracks'), many=True)
        artist = ma.Nested('ArtistSerializer', exclude=('album'))
        length = ma.Method('convert_time')
        
        _links = ma.Hyperlinks({
            'self':ma.URL('track', id='<id>'),
            'collection':ma.URL('track')
            })
        
        def convert_time(self, track):
            seconds = track.length
            mins = seconds//60
            seconds -= (mins*60)
            
            return "{!s:2>0}:{!s:2>0}".format(mins, seconds)
        
        class Meta:
            fields = ('id', 'artist', 'album', 'name', 'position', 'length', 'links', 'stream')
The only thing I'll note here is the use of Meta.fields rather than additional and that we're only exposing the streaming id for tracks to the serializer (and thus through the API) instead of both it and the file location. Giving away information like a file structure just makes it easier for people with malicious intent to gain access to the system.
In []:
#app/config.py
import os

basedir = os.path.abspath(os.path.dirname(__file__))

class BaseConfig:
    SQLALCHEMY_COMMIT_ON_TEARDOWN = True
    SQLALCHEMY_RECORD_QUERIES = False
    
    @staticmethod
    def init_app(app):
        pass

class DevConfig(BaseConfig):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV-DATABASE-URI') or 'sqlite:///{}'.format(os.path.join(basedir, 'dev-sqlite.db'))

configs = {
    'dev' : DevConfig,
    'default' : DevConfig
    }
In []:
#app/factory.py
from flask import Flask
from .config import configs

def create_app(env, exts=()):
    app = Flask(__name__)
    config = configs.get(env, 'default')
    app.config.from_object(config)
    config.init_app(app)
    
    for ext in exts:
        ext.init_app(app)
    
    return app
You'll see a lot of people turning to a factory for creating their application. However, they'll explicitly initialize their extensions one at a time. In this case, we pass in an environment and a list of extensions to initialize instead. I find that this is a little more flexible and doesn't rely on abusing scoping in Python.
In []:
#app/__init__.py
from flask import request, abort, send_file
from flask.ext.restful import Api, Resource
from .models import Artist, Album, Track
from .serializers import ArtistSerializer, AlbumSerializer, TrackSerializer
from .factory import create_app

app = create_app('dev', exts=[ma,db])
api = Api(app) # I've had strange things happen initializing the Restful API afterwards...

class SingleArtist(Resource):
    def get(self, id):
        artist = Artist.query.get_or_404(id)
        return {'artist' : ArtistSerializer(artist)}

class SingleAlbum(Resource):
    def get(self, id):
        album = Album.query.get_or_404(id)
        return {'album' : AlbumSerializer(artist)}

class SingleTrack(Resource):
    def get(self, id):
        track = Track.query.get_or_404(id)
        return {'track' : TrackSerializer(artist)}

class ListArtist(Resource):
    def get(self):
        page = request.args.get('page', default=1, type=int)
        limit = request.args.get('limit', default=10, type=int)
        
        q = Artist.query.order_by(Artist.name)
        q = q.paginate(page, limit, error_out=False)
        
        return {'artists' : ArtistSerializer(q.items, many=True)}

class ListAlbum(Resource):
    def get(self):
        page = request.args.get('page', default=1, type=int)
        limit = request.args.get('limit', default=10, type=int)
        
        q = Album.query.join(Artist).filter(Artist.id==Album.artist_id)
        q = q.order_by(Artist.name, Album.name)
        q = q.paginate(page, limit, error_out=False)
        
        return {'artists' : AlbumSerializer(q.items, many=True)}

class ListTrack(Resource):
    def get(self):
        page = request.args.get('page', default=1, type=int)
        limit = request.args.get('limit', default=10, type=int)
        
        q = Track.query.join(Artist, Album)
        q = q.filter(Artist.id==Album.artist_id, Album.id==Track.album_id)
        q = q.order_by(Artist.name, Album.name, Track.position)
        q = q.paginate(page, limit, error_out=False)
        
        return {'artists' : TrackSerializer(q.items, many=True)}

@app.route('/stream/<stream_id>/', methods=['GET'])
def stream(stream_id):
    track = Track.query.filter(Track.stream==stream_id).first()
    if not track:
        abort(404)
    
    return send_file(track.location)
    
api.add_resource(SingleArtist, '/artist/<int:id>/', endpoint='artist')
api.add_resource(ListArtist, '/artist/', endpoint='artists')

api.add_resource(SingleAlbum, '/album/<int:id>/', endpoint='album')
api.add_resource(ListAlbum, '/album/', endpoint='albums')

api.add_resource(SingleTrack, '/track/<int:id>/', endpoint='track')
api.add_resource(ListTrack, '/track/', endpoint='tracks')
There's a few differences here. I'll address a few:
  • Flask-SQLAlchemy provides a custom query object which locates our session for us. So, unlike regular SQLAlchemy, we don't have to directly use the session object. This can make querying feel a little odd if you're not used to it but ultimately, it acts the same.
  • When joining two models in SQLAlchemy, you need to explicitly filter on the join you'd like. There's multiple ways of doing this. I'll leave the others as an exercise to the reader.
  • Flask-SQLAlchemy includes a pagination helper object. It allows you to specify just a page and limit and paginate will return several helpful bits such as has_next, has_prev, next_num and prev_num among others.
  • abort allows you to redirect to an error page. Flask-SQLAlchemy implements a get_or_404 helper which does something similar.
  • send_file tells Flask to send a file response instead of something more typical.
This is all you need for your API to interact and stream. Except data. That's what manager.py is for.
In []:
#manager.py
from app import app, db, ma, models, serializers, utils
from flask.ext.script import Manager

manager = Manager(app)

@manager.option('-d', '--dir', dest='dir')
def add(dir):
    utils.store_directory(dir)

@manager.shell
def _shell_context():
    return dict(app=app, db=db, ma=ma, models=models, serials=serializers, utils=utils)

if __name__ == "__main__":
    manager.run()
Flask-Script is an incredibly useful extension that allows you to interact with your application on the command line. Consider this an extremely brief explaination.
  • Manager.option allows you to pass CLI arguments into your functions easily.
  • Manager.shell is a predetermined command that sets up a shell context for your application. All it requires is to know what to pass in.
The utils file is the only one I haven't covered yet. These are functions and things that will assist in the running of the application but are otherwise unconnected to anything else.
In []:
#app/utils.py
import os
from uuid import uuid4
from mutagenx import File
from .models import db, Album, Artist, Track

valid_types=('m4a', 'flac', 'mp3', 'ogg', 'oga')


def find(basedir, valid_types=valid_types):
    '''Utilize os.walk to only select out the files we'd like to potentially
    parse and yield them one at a time.'''
    basedir = os.path.abspath(basedir)
    for current, dirs, files in os.walk(basedir):
        if not files:
            continue
        for file in files:
            if file.endswith(valid_types):
                yield os.path.join(os.path.abspath(current), file)

def adaptor(track):
    return dict(
        artist=track['artist'][0],
        album=track['album'][0],
        position=int(track['tracknumber'][0].split('/')[0]),
        length=int(track.info.length),
        location=track.filename,
        name=track['title'][0],
        stream=str(uuid4())
        )

def adapt_track(track, adaptor=adaptor):

    info = adaptor(track)

    artist = Artist.query.filter(Artist.name == info['artist']).first()
    if not artist:
        artist = Artist(name=info['artist'])
        db.session.add(artist)
    info['artist'] = artist

    album = Album.query.filter(Album.name==info['album']).first()
    if not album:
        album = Album(name=info['album'], artist=artist)
        db.session.add(album)
    info['album'] = album

    track = Track.query.filter(Track.name==info['name'])
    track = track.filter(Track.album_id==album.id)
    track = track.first()
    if not track:
        track = Track(**info)
        db.session.add(track)

    return artist, album, track


def store_directory(basedir, valid_types=valid_types, adaptor=adaptor):
    for file in find(basedir, valid_types):
        file = File(file, easy=True)
        
        try:
            artist, album, track = adapt_track(file)
            print(
                " * Storing: {0.name} - {1.name} - {2.position:2>0} - {2.name}"
                "".format(artist, album, track))
            db.session.commit()
        except KeyError:
            pass
Now, to add information to the database, you'll simply do:
python manager.py add -d /home/justanr/Music/Acid Bath
And you'll get back something like this:
 * Storing: Acid Bath - When the Kite String Pops - 5 - Jezebel
 * Storing: Acid Bath - When the Kite String Pops - 11 - The Morticians Flame
 * Storing: Acid Bath - When the Kite String Pops - 14 - Cassie Eats Cockroaches
 * Storing: Acid Bath - When the Kite String Pops - 4 - Finger Paintings of the Insane
 * Storing: Acid Bath - When the Kite String Pops - 9 - Toubabo Koomi
 * Storing: Acid Bath - When the Kite String Pops - 13 - The Bones of Baby Dolls
 * Storing: Acid Bath - When the Kite String Pops - 6 - Scream of the Butterfly
 * Storing: Acid Bath - When the Kite String Pops - 10 - God Machine
 * Storing: Acid Bath - When the Kite String Pops - 2 - Tranquilized
 * Storing: Acid Bath - When the Kite String Pops - 8 - Dope Fiend
 * Storing: Acid Bath - When the Kite String Pops - 3 - Cheap Vodka
 * Storing: Acid Bath - When the Kite String Pops - 7 - Dr. Seuss Is Dead
 * Storing: Acid Bath - When the Kite String Pops - 12 - What Color Is Death
 * Storing: Acid Bath - When the Kite String Pops - 1 - The Blue
 * Storing: Acid Bath - Paegan Terrorism Tactics - 6 - Old Skin
 * Storing: Acid Bath - Paegan Terrorism Tactics - 10 - New Corpse
 * Storing: Acid Bath - Paegan Terrorism Tactics - 5 - Locust Spawning
 * Storing: Acid Bath - Paegan Terrorism Tactics - 12 - Ode of the Peagan
 * Storing: Acid Bath - Paegan Terrorism Tactics - 7 - New Death Sensation
 * Storing: Acid Bath - Paegan Terrorism Tactics - 11 - Dead Girl
 * Storing: Acid Bath - Paegan Terrorism Tactics - 4 - Diäb Soulé
 * Storing: Acid Bath - Paegan Terrorism Tactics - 3 - Graveflower
 * Storing: Acid Bath - Paegan Terrorism Tactics - 9 - 13 Fingers
 * Storing: Acid Bath - Paegan Terrorism Tactics - 2 - Bleed Me an Ocean
 * Storing: Acid Bath - Paegan Terrorism Tactics - 8 - Venus Blue
 * Storing: Acid Bath - Paegan Terrorism Tactics - 1 - Paegan Love Song
 * Storing: Acid Bath - Demos: 1993 - 1996 - 7 - Finger Paintings of the Insane
 * Storing: Acid Bath - Demos: 1993 - 1996 - 6 - Dope Fiend
 * Storing: Acid Bath - Demos: 1993 - 1996 - 8 - Jezebel
 * Storing: Acid Bath - Demos: 1993 - 1996 - 10 - Venus Blue
 * Storing: Acid Bath - Demos: 1993 - 1996 - 11 - Bleed Me an Ocean
 * Storing: Acid Bath - Demos: 1993 - 1996 - 3 - Scream of the Butterfly
 * Storing: Acid Bath - Demos: 1993 - 1996 - 9 - The Bones of Baby Dolls
 * Storing: Acid Bath - Demos: 1993 - 1996 - 2 - What Color Is Death
 * Storing: Acid Bath - Demos: 1993 - 1996 - 1 - Dr. Seuss Is Dead
 * Storing: Acid Bath - Demos: 1993 - 1996 - 5 - The Mortician's Flame
 * Storing: Acid Bath - Demos: 1993 - 1996 - 12 - Graveflower
 * Storing: Acid Bath - Demos: 1993 - 1996 - 4 - God Machine
Running the server is as simple as python manager.py runserver -p 5000 -h 0.0.0.0. From there navigating to 127.0.0.1:5000/artist/ will output something like this:
{
    "artists": [
        {
            "albums": [
                {
                    "id": 3,
                    "links": {
                        "collection": "/album/",
                        "self": "/album/3/"
                    },
                    "name": "Demos: 1993 - 1996",
                    "tracks": [
                        {
                            "id": 35,
                            "length": "05:39",
                            "links": {
                                "collection": "/track/",
                                "self": "/track/35/"
                            },
                            "name": "Dr. Seuss Is Dead",
                            "position": 1,
                            "stream": "5ef608d4-aca1-496d-a876-0778c2e6ea6b"
                        }
                    ]
                }
            ],
            "id": 1,
            "links": {
                "collection": "/artist/",
                "self": "/artist/1/"
            },
            "name": "Acid Bath"
        }
    ]
}
From there, navigating to 127.0.0.1:5000/stream/5ef608d4-aca1-496d-a876-0778c2e6ea6b/ will play the song.

From Here

There's many things wrong with the application. Flask is server static content, which is a job better served by a proper web server, such as Nginx. And we're just glossing over the issue of the application blowing up when it hits an object with only partial data. We're also storing objects one at a time instead of committing a batch of objects (say ten or fifteen at a time, or even a whole album or artist discography).
But this also lays the foundations of a much broader application. There's several features I intend on adding and building on to here until there's a full featured API.
But for now, feel free to tinker and change. If you develop something, submit me a pull request.

Sunday, August 24, 2014

Basic APIs Part One: Mocking and returning a data source

Building Restful APIs can be hard. But with Flask and a few extensions, they don't need to be. For this brief tutorial, you'll need to install:

All of these are available on PyPI and ergo pip (and easy_install if that's your jam instead).

Okay, someone's looking at me confused right now because Marshmallow and Restful overlap in the marshalling department. I prefer Marshmallow's powerful serializing abilities to, in my opinion, Restful's more limited approach. Why Flask-Marshmallow? It can introspect endpoints and dynamically create links.

You'll notice that there's no database required for this, that's because we're focusing on building the API rather than where the data comes from. I'll also admit, there's quite a bit of repeated code in here, again the focus was on the API, not the other parts.

Setup

First, we'll have to setup the app and the two extensions.

In []:
from flask import Flask
from flask.ext.restful import Api, Resource
from flask.ext.marshmallow import Marshmallow

app = Flask(__name__)
api = Api(app)
ma = Marshmallow(app)

Data Source

First, our data source. I'm using basic objects here simply to not worry about connecting to any external sources. There are three:

  • Artist
  • Album
  • Track

We're going to model a basic music system (as in, extremely basic). The objects themselves are extremely basic and essentially just dictionaries in this case, but they're standing in for actual data models (say, for SQLAlchemy). They're circularly linked, such that:

>>> artist = artists[0]
>>> artist.albums[0].artist is artist
... True
>>> artist.albums[0].tracks[0].artist is artist
... True

And so on with the albums and tracks. However, since we're managing these by hands...it isn't much fun.

In []:
class Artist(object):
    def __init__(self, id, name, albums=None):
        self.id = id
        self.name = name
        self.albums = albums or []

class Album(object):
    def __init__(self, id, name, artist=None, tracks=None):
        self.id = id
        self.name = name
        self.artist = artist
        self.tracks = tracks or []

class Track(object):
    def __init__(self, id, name, position, length, artist=None, albums=None):
        self.id = id
        self.name = name
        self.length = length # length in seconds
        self.position = position
        self.artist = artist
        self.albums = albums or [] # tracks can appear on multiple albums

Flask-Marshmallow

Setting up serializers with Marshmallow and it's Flask extension is a breeze and an absolute joy. Don't consider this a tour de force but as a short introduction to the main features of them.

diff Marshmallow Flask-Marshmallow

The biggest change to Flask-Marshmallow is that all the fields are built into your Flask-Marshmallow object. Before you had to use from marshmallow import Serializer, fields. Flask-Marshmallow also adds a URL field and a helper method for building a collection of links.

So, let's set them up.

In []:
class ArtistSerializer(ma.Serializer):
    albums = ma.Nested('AlbumSerializer', exclude=('artist',), many=True)
    
    _links = ma.Hyperlinks({
        'self':ma.URL('artist', id='<id>'),
        'collection':ma.URL('artists')
        })
    
    class Meta:
        additional = ('id', 'name')

class AlbumSerializer(ma.Serializer):
    artist = ma.Nested('ArtistSerializer', exclude=('albums',))
    tracks = ma.Nested('TrackSerializer', exclude=('albums', 'artist'), many=True)
    
    _links = ma.Hyperlinks({
        'self':ma.URL('album', id='<id>'),
        'collection':ma.URL('albums')
        })
    
    class Meta:
        additional = ('id', 'name')

class TrackSerializer(ma.Serializer):
        albums = ma.Nested('AlbumSerializer', exclude=('artist', 'tracks'), many=True)
        artist = ma.Nested('ArtistSerializer', exclude=('albums'))
        length = ma.Method('convert_time')
        
        _links = ma.Hyperlinks({
            'self':ma.URL('track', id='<id>'),
            'collection':ma.URL('tracks')
            })
        
        def convert_time(self, track):
            seconds = track.length
            mins = seconds//60
            seconds -= (mins*60)
            
            return "{!s:2>0}:{!s:2>0}".format(mins, seconds)
        
        class Meta:
            addition = ('id', 'name', 'position')

Just to briefly go over this:

  • Serializers subclass the main ma.Serializer class where the actual magic happens. All we're concerned about is telling it which fields it should look for and where it's at.
  • ma.Nested allows you to nest serializer by either passing in a class or in >= 0.6.0 you can pass in the name of the serializer, which is also the greatest thing ever because before you had to define your base serializers and then subclass them to add in any nested serializers that are defined afterwards. The many keyword tells the serializer to expect an iterable and run the nested serializer over all the items in it.
  • ma.Method runs a method in the serializer and passes it the object it's attempting to serialize into it
  • ma.Hyperlinks is like a serializer itself in that it'll accept a dictionary of fields and introspect it to output data.
  • ma.URL is like Flask.url_for in that'll it accepts an endpoint and any arguments for the endpoint to create a url for it.
  • The class Meta definition in those classes is actually a shortcut for properties that Marshmallow can automatically marshal. Artist.Meta is essentially a short cut for:
    • id = ma.Integer('id')
    • name = ma.String('name')
  • Serializers themselves are used via: MySerializer(myobject) or MySerializer([myobject, myobject2], many=True) The new object has one property that we're interested in: Serializer.data which returns an OrderedDict of all the fields processed

Doing this with Flask-Restful is not as easy or as much fun... Restful's serializers are stores in dictionaries rather than in a custom container.

In []:
from flask.ext.restful import fields, marshal, marshal_with

class HumanReadableTime(fields.Raw):
    def format(self, value):
        mins = value//60
        seconds -= (mins*60)
        return "{!s:2>0}:{!s:2>0}".format(mins, seconds)

artist_fields = {'name':fields.String, 'id':fields.Integer}
album_fields = {'name':fields.String, 'id':fields.Integer}
track_fields = {
    'name':fields.String, 'id':fields.Integer, 'position':fields.Integer,
    'length':HumanReadableTime, 'artist':fields.Nested(artist_fields),
    'albums':fields.List(fields.Nested(album_fields))
    }

Nesting structures is similar to Marshmallow except there's no option to exclude fields, which leads to painful tricks like creating dictionaries on the fly or absuing ChainMap. This quickly spirals out of control. All to avoid recursively serialization. Whereas with Marshmallow's serializers it's as easy as passing in the fields you want to exclude.

Let's build some data, by hand, to throw at our serializers. I'm a big fan of death metal, especially the stranger, dissonant corners of it. However, feel free to sub in any other bands, albums and tracks you like.

In []:
from itertools import chain

immolation = Artist(id=1, name='Immolation')
gorguts = Artist(id=2, name='Gorguts')
ulcerate = Artist(id=3, name='Ulcerate')

artists = [immolation, gorguts, ulcerate]

ctawb = Album(id=1, name='Closer to a World Below', artist=immolation)
majesty_and_decay = Album(id=2, name='Majesty and Decay', artist=immolation)
obscura = Album(id=3, name='Obscura', artist=gorguts)
eif = Album(id=4, name='Everything is Fire', artist=ulcerate)

immolation.albums.extend([ctawb, majesty_and_decay])
gorguts.albums.append(obscura)
ulcerate.albums.append(eif)

albums = [ctawb, majesty_and_decay, obscura, eif]

def build_tracks(tracks, artist, albums, offset=0):
    '''Accepts an iterable of track parts to construct track objects 
    and then attaches the tracklist to each album passed to the function.
    '''
    
    tracks = [Track(
        id=i+offset+1, position=i+1, artist=artist, albums=albums,
        name=t[0], length=t[1]
        ) for i, t in enumerate(tracks)]
    
    for a in albums:
        a.tracks.extend(tracks)

        
offset = 0

ctawb_tracks = [
    ('Higher, Coward', 300), ('Father, You\'re Not a Father', 303),
    ('Furthest From the Truth', 266), ('Fall From a High Place', 277),
    ('Unpardonable Sin', 473), ('Lost Passion', 340),                           
    ('Put My Hand in the Fire', 252), ('Close to a World Below', 499)
    ]
build_tracks(ctawb_tracks, artist=immolation, albums=[ctawb], offset=offset)

offset += len(ctawb_tracks)
mad_tracks = [
    ('Intro', 79), ('The Purge', 199), ('A Token of Malice', 161),
    ('Majesty and Decay', 269), ('Divine Code', 219), ('The Human Form', 244),
    ('A Glorious Epoch', 278), ('Interlude', 124),
    ('A Thunderous Consequence', 239), ('The Rapture of Ghosts', 519),
    ('Power and Shame', 224), ('The Comfort of Cowards', 552)
    ]
build_tracks(mad_tracks, artist=immolation, albums=[majesty_and_decay], offset=offset)

offset += len(mad_tracks)
obscura_tracks = [
    ('Obscura', 244), ('Earthly Love', 244), ('The Carnal State', 188),
    ('Nostalgia', 370), ('The Art of Somber Ecstasy', 260),
    ('Clouded', 572), ('Subtle Body', 203), ('Rapturous Grief', 327),           
    ('La Vie Est Prelud... (La Morte Orgasme)', 208),
    ('Illuminatus', 375), ('Faceless Ones', 230), ('Sweet Silence', 405)
    ]

offset += len(obscura_tracks)
eif_tracks = [
    ('Drown Within', 402), ('We are Nil', 541), ('Withered and Obsolete', 370),
    ('Caecus', 386), ('Tyranny', 522), ('The Earth at Its Knees', 545),
    ('Soulessness Embraced', 366), ('Everything is Fire', 472)
    ]
build_tracks(eif_tracks, artist=ulcerate, albums=[eif], offset=offset)

tracks = list(chain.from_iterable(a.tracks for a in albums))

Holy crap that's a lot of hassle to go through just to mockup some data. But, it's there. How do the serializers handle?

In []:
# ArtistSerialize(immolation, exclude=('albums',)).data
{
    "artist": {
        "_links": {
            "collection": "/artist/",
            "self": "/artist/1/"
        },
        "id": 1,
        "name": "Immolation"
    }
}
In []:
# TrackSerialize(tracks[:2], many=True).data
{
    "tracks": [
        {
            "_links": {
                "collection": "/track/",
                "self": "/track/1/"
            },
            "albums": [
                {
                    "_links": {
                        "collection": "/album/",
                        "self": "/album/1/"
                    },
                    "id": 1,
                    "name": "Closer to a World Below"
                }
            ],
            "artist": {
                "_links": {
                    "collection": "/artist/",
                    "self": "/artist/1/"
                },
                "id": 1,
                "name": "Immolation"
            },
            "id": 1,
            "length": "05:00",
            "name": "Higher, Coward",
            "position": 1
        },
        {
            "_links": {
                "collection": "/track/",
                "self": "/track/2/"
            },
            "albums": [
                {
                    "_links": {
                        "collection": "/album/",
                        "self": "/album/1/"
                    },
                    "id": 1,
                    "name": "Closer to a World Below"
                }
            ],
            "artist": {
                "_links": {
                    "collection": "/artist/",
                    "self": "/artist/1/"
                },
                "id": 1,
                "name": "Immolation"
            },
            "id": 2,
            "length": "05:03",
            "name": "Father, You're Not a Father",
            "position": 2
        }
    ]
}

API Endpoints

Flask-RESTful makes building an API stupid easy. It does almost all the hardwork for you, leaving you to deal with just the implementation details of the API. However, this example won't be showing off the niceties of Flask-RESTful as the API endpoints just implement a GET HTTP method. I'll save the others for a later example.

In []:
class SingleArtist(Resource):
    def get(self, id):
        return {'artist':ArtistSerializer(artists[id-1]).data}

class SingleAlbum(Resource):
    def get(self, id):
        return {'album':AlbumSerializer(albums[id-1]).data}

class SingleTrack(Resource):
    def get(self, id):
        return {'track':TrackSerializer(tracks[id-1]).data}

class ListArtist(Resource):
    def get(self):
        return {'artists':ArtistSerializer(artists, many=True).data}

class ListAlbum(Resource):
    def get(self):
        return {'albums':AlbumSerializer(albums, many=True).data}

class ListTrack(Resource):
    def get(self):
        return {'tracks':TrackSerializer(tracks, many=True).data}

api.add_resource(SingleArtist, '/artist/<int:id>/', endpoint='artist')
api.add_resource(ListArtist, '/artist/', endpoint='artists')

api.add_resource(SingleAlbum, '/album/<int:id>/', endpoint='album')
api.add_resource(ListAlbum, '/album/', endpoint='albums')

api.add_resource(SingleTrack, '/track/<int:id>/', endpoint='track')
api.add_resource(ListTrack, '/track/', endpoint='tracks')

if __name__ == '__main__':
    app.run(debug=True, port=5000, host='0.0.0.0')

That's It.

After all this is to run your app somewhere. Either when the script is __main__ or importing the app into another script to run it. Have fun, play with it, modify it and see what you can get it to do.