Skip to content

Commit

Permalink
API Docs improvements (#734)
Browse files Browse the repository at this point in the history
* Turn off Swagger mask

* Add better docs support

* Fix weird spacing

* Fix up and add duration

* Fix swagger over HTTPs

* Add TODO comment

* Fix https check

* Remove TODO

* Return empty data for no tracks

Co-authored-by: Michael Piazza <michael.piazza.mp@gmail.com>
  • Loading branch information
raymondjacobson and piazzatron committed Aug 7, 2020
1 parent f126e2c commit 2f0b941
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 24 deletions.
14 changes: 13 additions & 1 deletion discovery-provider/src/api/v1/api.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
from flask import Flask, Blueprint
from flask.helpers import url_for
from flask_restx import Resource, Api
from src.api.v1.users import ns as users_ns
from src.api.v1.playlists import ns as playlists_ns
from src.api.v1.tracks import ns as tracks_ns
from src.api.v1.metrics import ns as metrics_ns
from src.api.v1.models.users import ns as models_ns

class ApiWithHTTPS(Api):
@property
def specs_url(self):
"""
Monkey patch for HTTPS or else swagger docs do not serve over HTTPS
https://stackoverflow.com/questions/47508257/serving-flask-restplus-on-https-server
"""
scheme = 'https' if 'https' in self.base_url else 'http'
return url_for(self.endpoint('specs'), _external=True, _scheme=scheme)


bp = Blueprint('api_v1', __name__, url_prefix="/v1")
api_v1 = Api(bp, version='1.0', description='Audius V1 API')
api_v1 = ApiWithHTTPS(bp, version='1.0', description='Audius V1 API')
api_v1.add_namespace(models_ns)
api_v1.add_namespace(users_ns)
api_v1.add_namespace(playlists_ns)
Expand Down
11 changes: 9 additions & 2 deletions discovery-provider/src/api/v1/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ def extend_repost(repost):

def extend_favorite(favorite):
favorite["user_id"] = encode_int_id(favorite["user_id"])
favorite["save_item_id"] = encode_int_id(favorite["save_item_id"])
favorite["favorite_item_id"] = encode_int_id(favorite["save_item_id"])
favorite["favorite_type"] = favorite["save_type"]
return favorite

def extend_remix_of(remix_of):
Expand All @@ -114,10 +115,15 @@ def extend_track(track):
track["user"] = extend_user(track["user"])
track["id"] = track_id
track["user_id"] = owner_id
track["followee_saves"] = list(map(extend_favorite, track["followee_saves"]))
track["followee_favorites"] = list(map(extend_favorite, track["followee_saves"]))
track["followee_resposts"] = list(map(extend_repost, track["followee_reposts"]))
track = add_track_artwork(track)
track["remix_of"] = extend_remix_of(track["remix_of"])
track["favorite_count"] = track["save_count"]
duration = 0.
for segment in track["track_segments"]:
duration += segment["duration"]
track["duration"] = duration
return track

def extend_playlist(playlist):
Expand All @@ -128,6 +134,7 @@ def extend_playlist(playlist):
if ("user" in playlist):
playlist["user"] = extend_user(playlist["user"])
playlist = add_playlist_artwork(playlist)
playlist["favorite_count"] = playlist["save_count"]
return playlist

def abort_not_found(identifier, namespace):
Expand Down
18 changes: 12 additions & 6 deletions discovery-provider/src/api/v1/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@
ns = Namespace("Models")

repost = ns.model('repost', {
"repost_item_id": fields.String(required=True),
"repost_type": fields.String(required=True),
"user_id": fields.String(required=True)
})

repost_full = ns.clone("repost_full", repost, {
"blockhash": fields.String(required=True),
"blocknumber": fields.Integer(required=True),
"created_at": fields.String(required=True),
"is_current": fields.Boolean(required=True),
"is_delete": fields.Boolean(required=True),
"repost_item_id": fields.String(required=True),
"repost_type": fields.String(required=True),
"user_id": fields.String(required=True)
})

favorite = ns.model('favorite', {
"favorite_item_id": fields.String(required=True),
"favorite_type": fields.String(required=True),
"user_id": fields.String(required=True)
})

favorite_full = ns.clone("favorite_full", favorite, {
"blockhash": fields.String(required=True),
"blocknumber": fields.Integer(required=True),
"created_at": fields.String(required=True),
"is_current": fields.Boolean(required=True),
"is_delete": fields.Boolean(required=True),
"save_item_id": fields.String(required=True),
"save_type": fields.String(required=True),
"user_id": fields.String(required=True)
})

version_metadata = ns.model("version_metadata", {
Expand Down
2 changes: 1 addition & 1 deletion discovery-provider/src/api/v1/models/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"is_album": fields.Boolean(required=True),
"playlist_name": fields.String(required=True),
"repost_count": fields.Integer(required=True),
"save_count": fields.Integer(required=True),
"favorite_count": fields.Integer(required=True),
"user": fields.Nested(user_model, required=True),
})

Expand Down
9 changes: 5 additions & 4 deletions discovery-provider/src/api/v1/models/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,15 @@
"description": fields.String,
"genre": fields.String,
"id": fields.String(required=True),
"length": fields.Integer,
"mood": fields.String,
"release_date": fields.String,
"remix_of": fields.Nested(remix_parent),
"repost_count": fields.Integer(required=True),
"route_id": fields.String(required=True),
"save_count": fields.Integer(required=True),
"favorite_count": fields.Integer(required=True),
"tags": fields.String,
"title": fields.String(required=True),
"user": fields.Nested(user_model, required=True),
"duration": fields.Float(required=True)
})

track_full = ns.clone('track_full', track, {
Expand All @@ -65,6 +64,7 @@
"credits_splits": fields.String,
"download": fields.Nested(download),
"isrc": fields.String,
"length": fields.Integer,
"license": fields.String,
"iswc": fields.String,
"field_visibility": fields.Nested(field_visibility),
Expand All @@ -74,8 +74,9 @@
"is_delete": fields.Boolean(required=True),
"is_unlisted": fields.Boolean(required=True),
"has_current_user_saved": fields.Boolean(required=True),
"followee_saves": fields.List(fields.Nested(favorite), required=True),
"followee_favorites": fields.List(fields.Nested(favorite), required=True),
"file_type": fields.String,
"route_id": fields.String(required=True),
"blocknumber": fields.Integer(required=True),
"metadata_multihash": fields.String(required=True),
"stem_of": fields.Nested(stem_parent),
Expand Down
33 changes: 30 additions & 3 deletions discovery-provider/src/api/v1/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@
@ns.route("/<string:playlist_id>")
class Playlist(Resource):
@record_metrics
@ns.doc(
id="""Get Playlist""",
params={'playlist_id': 'A Playlist ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(playlists_response)
@cache(ttl_sec=5)
def get(self, playlist_id):
"""Fetch a playlist"""
"""Fetch a playlist."""
playlist_id = decode_with_abort(playlist_id, ns)
args = {"playlist_id": [playlist_id], "with_users": True}
playlists = get_playlists(args)
Expand All @@ -36,10 +45,19 @@ def get(self, playlist_id):
@ns.route("/<string:playlist_id>/tracks")
class PlaylistTracks(Resource):
@record_metrics
@ns.doc(
id="""Get Playlist Tracks""",
params={'playlist_id': 'A Playlist ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(playlist_tracks_response)
@cache(ttl_sec=5)
def get(self, playlist_id):
"""Fetch tracks within a playlist"""
"""Fetch tracks within a playlist."""
decoded_id = decode_with_abort(playlist_id, ns)
args = {"playlist_id": decoded_id, "with_users": True}
playlist_tracks = get_playlist_tracks(args)
Expand All @@ -53,11 +71,20 @@ def get(self, playlist_id):
@ns.route("/search")
class PlaylistSearchResult(Resource):
@record_metrics
@ns.doc(
id="""Search Playlists""",
params={'query': 'Search Query'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(playlist_search_result)
@ns.expect(search_parser)
@cache(ttl_sec=5)
def get(self):
"""Search for a playlist"""
"""Search for a playlist."""
args = search_parser.parse_args()
query = args["query"]
search_args = {
Expand Down
49 changes: 46 additions & 3 deletions discovery-provider/src/api/v1/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,19 @@
@ns.route('/<string:track_id>')
class Track(Resource):
@record_metrics
@ns.doc(
id="""Get Track""",
params={'track_id': 'A Track ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(track_response)
@cache(ttl_sec=5)
def get(self, track_id):
"""Fetch a track"""
"""Fetch a track."""
decoded_id = decode_with_abort(track_id, ns)
args = {"id": [decoded_id], "with_users": True, "filter_deleted": True}
tracks = get_tracks(args)
Expand All @@ -41,9 +50,25 @@ def get(self, track_id):
@ns.route("/<string:track_id>/stream")
class TrackStream(Resource):
@record_metrics
@ns.doc(
id="""Stream Track""",
params={'track_id': 'A Track ID'},
responses={
200: 'Success',
216: 'Partial content',
400: 'Bad request',
416: 'Content range invalid',
500: 'Server error'
}
)
@cache(ttl_sec=5)
def get(self, track_id):
"""Redirect to track mp3"""
"""
Get the track's streamable mp3 file.
This endpoint accepts the Range header for streaming.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
"""
decoded_id = decode_with_abort(track_id, ns)
args = {"track_id": decoded_id}
creator_nodes = get_track_user_creator_node(args)
Expand All @@ -65,10 +90,20 @@ def get(self, track_id):
@ns.route("/search")
class TrackSearchResult(Resource):
@record_metrics
@ns.doc(
id="""Search Tracks""",
params={'query': 'Search Query'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(track_search_result)
@ns.expect(search_parser)
@cache(ttl_sec=60)
def get(self):
"""Search for a track."""
args = search_parser.parse_args()
query = args["query"]
search_args = {
Expand All @@ -87,10 +122,18 @@ def get(self):
@ns.route("/trending")
class Trending(Resource):
@record_metrics
@ns.doc(
id="""Trending Tracks""",
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(tracks_response)
@cache(ttl_sec=30 * 60)
def get(self):
"""Get the trending tracks"""
"""Gets the top 100 trending (most popular) tracks on Audius"""
args = trending_parser.parse_args()
time = args.get("time") if args.get("time") is not None else 'week'
args = {
Expand Down
42 changes: 38 additions & 4 deletions discovery-provider/src/api/v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,19 @@
@ns.route("/<string:user_id>")
class User(Resource):
@record_metrics
@ns.doc(
id="""Get User""",
params={'user_id': 'A User ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(user_response)
@cache(ttl_sec=5)
def get(self, user_id):
"""Fetch a single user"""
"""Fetch a single user."""
decoded_id = decode_with_abort(user_id, ns)
args = {"id": [decoded_id]}
users = get_users(args)
Expand All @@ -36,6 +45,15 @@ def get(self, user_id):
@ns.route("/<string:user_id>/tracks")
class TrackList(Resource):
@record_metrics
@ns.doc(
id="""Get User's Tracks""",
params={'user_id': 'A User ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(tracks_response)
@cache(ttl_sec=5)
def get(self, user_id):
Expand All @@ -44,14 +62,21 @@ def get(self, user_id):
args = {"user_id": decoded_id, "with_users": True, "filter_deleted": True}
tracks = get_tracks(args)
tracks = list(map(extend_track, tracks))
if not tracks:
abort_not_found(user_id, ns)
return success_response(tracks)

favorites_response = make_response("favorites_response", ns, fields.List(fields.Nested(favorite)))
@ns.route("/<string:user_id>/favorites")
class FavoritedTracks(Resource):
@record_metrics
@ns.doc(
id="""Get User's Favorite Tracks""",
params={'user_id': 'A User ID'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(favorites_response)
@cache(ttl_sec=5)
def get(self, user_id):
Expand All @@ -66,10 +91,19 @@ def get(self, user_id):
@ns.route("/search")
class UserSearchResult(Resource):
@record_metrics
@ns.doc(
id="""Search Users""",
params={'query': 'Search query'},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(user_search_result)
@ns.expect(search_parser)
def get(self):
"""Seach for a user"""
"""Seach for a user."""
args = search_parser.parse_args()
query = args["query"]
search_args = {
Expand Down
4 changes: 4 additions & 0 deletions discovery-provider/src/utils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def parse_flask_section(self):
if ('url_read_replica' not in current_app.config['db']) or (not current_app.config['db']['url_read_replica']):
current_app.config['db']['url_read_replica'] = current_app.config['db']['url']

# Always disable (not included in app.default_config)
# See https://flask-restx.readthedocs.io/en/latest/mask.html#usage
current_app.config['RESTX_MASK_SWAGGER'] = False

def _load_item(self, section_name, key):
"""Load the specified item from the [flask] section. Type is
determined by the type of the equivalent value in app.default_config
Expand Down

0 comments on commit 2f0b941

Please sign in to comment.