Skip to content

Commit

Permalink
Full Users API (#878)
Browse files Browse the repository at this point in the history
* Add full user follows and following routes to DP v1 api

* Add full users endpoints

* Clean up and rebase

* Remove logs

* Fix remix user info

* Add legacy CID support

* Fix non success response

Co-authored-by: jowlee <joeylee0925@gmail.com>
  • Loading branch information
raymondjacobson and jowlee committed Oct 2, 2020
1 parent d84c860 commit 97bb6d3
Show file tree
Hide file tree
Showing 13 changed files with 319 additions and 60 deletions.
6 changes: 6 additions & 0 deletions discovery-provider/src/api/v1/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ def add_playlist_artwork(playlist):
return playlist

def add_user_artwork(user):
# Legacy CID-only references to images
user["cover_photo_legacy"] = user["cover_photo"]
user["profile_picture_legacy"] = user["profile_picture"]

endpoint = get_primary_endpoint(user)
if not endpoint:
return user
Expand Down Expand Up @@ -104,6 +108,8 @@ def extend_remix_of(remix_of):
def extend_track_element(track):
track_id = track["parent_track_id"]
track["parent_track_id"] = encode_int_id(track_id)
if ("user" in track):
track["user"] = extend_user(track["user"])
return track

if not remix_of or not "tracks" in remix_of or not remix_of["tracks"]:
Expand Down
4 changes: 3 additions & 1 deletion discovery-provider/src/api/v1/models/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,7 @@
"is_creator": fields.Boolean(required=True),
"updated_at": fields.String(required=True),
"cover_photo_sizes": fields.String,
"profile_picture_sizes": fields.String
"cover_photo_legacy": fields.String,
"profile_picture_sizes": fields.String,
"profile_picture_legacy": fields.String
})
2 changes: 1 addition & 1 deletion discovery-provider/src/api/v1/tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def get_trending(args):
}

# decode and add user_id if necessary
if (current_user_id):
if current_user_id:
decoded_id = decode_string_id(current_user_id)
args["current_user_id"] = decoded_id

Expand Down
193 changes: 183 additions & 10 deletions discovery-provider/src/api/v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
from src.queries.get_followers_for_user import get_followers_for_user

from src.api.v1.helpers import abort_not_found, decode_with_abort, extend_favorite, extend_track, \
extend_user, make_response, search_parser, success_response, abort_bad_request_param, \
get_default_max, encode_int_id, decode_string_id
from .models.tracks import track
extend_user, format_limit, format_offset, make_response, search_parser, success_response, abort_bad_request_param, \
get_default_max, decode_string_id
from .models.tracks import track, track_full
from src.utils.redis_cache import cache
from src.utils.redis_metrics import record_metrics

Expand All @@ -24,7 +24,23 @@
full_ns = Namespace('users', description='Full user operations')

user_response = make_response("user_response", ns, fields.Nested(user_model))
@ns.route("/<string:user_id>")
full_user_response = make_response(
"full_user_response", full_ns, fields.List(fields.Nested(user_model_full)))

def get_single_user(user_id, current_user_id):
args = {
"id": [user_id],
"current_user_id": current_user_id
}
users = get_users(args)
if not users:
abort_not_found(user_id, ns)
user = extend_user(users[0])
return success_response(user)


USER_ROUTE = "/<string:user_id>"
@ns.route(USER_ROUTE)
class User(Resource):
@record_metrics
@ns.doc(
Expand All @@ -40,21 +56,68 @@ class User(Resource):
@cache(ttl_sec=5)
def get(self, user_id):
"""Fetch a single user."""
decoded_id = decode_with_abort(user_id, ns)
args = {"id": [decoded_id]}
user_id = decode_with_abort(user_id, ns)
return get_single_user(user_id, None)

full_user_parser = reqparse.RequestParser()
full_user_parser.add_argument('user_id', required=False)
@full_ns.route(USER_ROUTE)
class FullUser(Resource):
@record_metrics
@full_ns.marshal_with(full_user_response)
@cache(ttl_sec=5)
def get(self, user_id):
user_id = decode_with_abort(user_id, ns)
args = full_user_parser.parse_args()
current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

return get_single_user(user_id, current_user_id)

full_user_handle_parser = reqparse.RequestParser()
full_user_handle_parser.add_argument('user_id', required=False)
@full_ns.route("/handle/<string:handle>")
class FullUserHandle(Resource):
@record_metrics
@full_ns.marshal_with(full_user_response)
@cache(ttl_sec=5)
def get(self, handle):
args = full_user_handle_parser.parse_args()
current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

args = {
"handle": handle,
"current_user_id": current_user_id
}
users = get_users(args)
if not users:
abort_not_found(user_id, ns)
abort_not_found(handle, ns)
user = extend_user(users[0])
return success_response(user)

USER_TRACKS_ROUTE = "/<string:user_id>/tracks"
user_tracks_route_parser = reqparse.RequestParser()
user_tracks_route_parser.add_argument('user_id', required=False)
user_tracks_route_parser.add_argument('limit', required=False, type=int)
user_tracks_route_parser.add_argument('offset', required=False, type=int)
user_tracks_route_parser.add_argument(
'sort', required=False, type=str, default='date', choices=('date', 'plays'))

tracks_response = make_response("tracks_response", ns, fields.List(fields.Nested(track)))
@ns.route("/<string:user_id>/tracks")
@ns.route(USER_TRACKS_ROUTE)
class TrackList(Resource):
@record_metrics
@ns.doc(
id="""Get User's Tracks""",
params={'user_id': 'A User ID'},
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
'sort': 'Sort mode'
},
responses={
200: 'Success',
400: 'Bad request',
Expand All @@ -66,11 +129,121 @@ class TrackList(Resource):
def get(self, user_id):
"""Fetch a list of tracks for a user."""
decoded_id = decode_with_abort(user_id, ns)
args = {"user_id": decoded_id, "with_users": True, "filter_deleted": True}
args = user_tracks_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

sort = args.get("sort", None)
offset = format_offset(args)
limit = format_limit(args)

args = {
"user_id": decoded_id,
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"sort": sort,
"limit": limit,
"offset": offset
}
tracks = get_tracks(args)
tracks = list(map(extend_track, tracks))
return success_response(tracks)

full_tracks_response = make_response("full_tracks", full_ns, fields.List(fields.Nested(track_full)))
@full_ns.route(USER_TRACKS_ROUTE)
class FullTrackList(Resource):
@record_metrics
@ns.doc(
id="""Get User's Tracks""",
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
'sort': 'Sort mode'
},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(full_tracks_response)
@cache(ttl_sec=5)
def get(self, user_id):
"""Fetch a list of tracks for a user."""
decoded_id = decode_with_abort(user_id, ns)
args = user_tracks_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

sort = args.get("sort", None)
offset = format_offset(args)
limit = format_limit(args)

args = {
"user_id": decoded_id,
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"sort": sort,
"limit": limit,
"offset": offset
}
tracks = get_tracks(args)
tracks = list(map(extend_track, tracks))
return success_response(tracks)


@full_ns.route("/handle/<string:handle>/tracks")
class HandleFullTrackList(Resource):
@record_metrics
@ns.doc(
id="""Get User's Tracks""",
params={
'user_id': 'A User ID',
'limit': 'Limit',
'offset': 'Offset',
'sort': 'Sort mode'
},
responses={
200: 'Success',
400: 'Bad request',
500: 'Server error'
}
)
@ns.marshal_with(full_tracks_response)
@cache(ttl_sec=5)
def get(self, handle):
"""Fetch a list of tracks for a user."""
args = user_tracks_route_parser.parse_args()

current_user_id = None
if args.get("user_id"):
current_user_id = decode_string_id(args.get("user_id"))

sort = args.get("sort", None)
offset = format_offset(args)
limit = format_limit(args)

args = {
"handle": handle,
"current_user_id": current_user_id,
"with_users": True,
"filter_deleted": True,
"sort": sort,
"limit": limit,
"offset": offset
}
tracks = get_tracks(args)
tracks = list(map(extend_track, tracks))
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):
Expand Down
13 changes: 13 additions & 0 deletions discovery-provider/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,19 @@ def __repr__(self):
updated_at={self.updated_at}\
created_at={self.created_at}>"

class AggregatePlays(Base):
__tablename__ = "aggregate_plays"

play_item_id = Column(Integer, primary_key=True, nullable=False, index=True)
count = Column(Integer, nullable=False, index=False)

Index('play_item_id_idx', 'play_item_id', unique=False)

def __repr__(self):
return f"<AggregatePlays(\
play_item_id={self.play_iteme_id},\
count={self.count}>"

class RouteMetrics(Base):
__tablename__ = "route_metrics"

Expand Down
3 changes: 3 additions & 0 deletions discovery-provider/src/queries/get_top_genre_users.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import logging
from sqlalchemy import func, asc, desc

from src.models import User, Track, Follow
from src.utils import helpers
from src.utils.db_session import get_db_read_replica
from src.queries.query_helpers import populate_user_metadata, paginate_query

logger = logging.getLogger(__name__)


def get_top_genre_users(args):
genres = []
Expand Down
30 changes: 23 additions & 7 deletions discovery-provider/src/queries/get_tracks.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import logging # pylint: disable=C0302

from src.models import Track
from src.models import AggregatePlays, Track, User
from src.utils import helpers
from src.utils.db_session import get_db_read_replica
from src.queries.query_helpers import get_current_user_id, paginate_query, parse_sort_param, \
from src.queries.query_helpers import paginate_query, parse_sort_param, \
populate_track_metadata, get_users_ids, get_users_by_id

logger = logging.getLogger(__name__)

def get_tracks(args):
tracks = []
current_user_id = args.get("current_user_id")

db = get_db_read_replica()
with db.scoped_session() as session:
if "handle" in args:
handle = args.get("handle")
user_id = session.query(User.user_id).filter(User.handle == handle).first()
args["user_id"] = user_id

# Create initial query
base_query = session.query(Track)
base_query = base_query.filter(Track.is_current == True, Track.is_unlisted == False, Track.stem_of == None)
Expand Down Expand Up @@ -47,21 +54,30 @@ def get_tracks(args):
Track.blocknumber >= min_block_number
)

whitelist_params = ['created_at', 'create_date', 'release_date', 'blocknumber', 'track_id']
base_query = parse_sort_param(base_query, Track, whitelist_params)
if "sort" in args:
if args["sort"] == "date":
base_query = base_query.order_by(Track.created_at.desc(), Track.track_id.desc())
elif args["sort"] == "plays":
base_query = base_query.join(
AggregatePlays,
AggregatePlays.play_item_id == Track.track_id
).order_by(
AggregatePlays.count.desc()
)
else:
whitelist_params = ['created_at', 'create_date', 'release_date', 'blocknumber', 'track_id']
base_query = parse_sort_param(base_query, Track, whitelist_params)
query_results = paginate_query(base_query).all()
tracks = helpers.query_result_to_list(query_results)

track_ids = list(map(lambda track: track["track_id"], tracks))

current_user_id = get_current_user_id(required=False)

# bundle peripheral info into track results
tracks = populate_track_metadata(session, track_ids, tracks, current_user_id)

if args.get("with_users", False):
user_id_list = get_users_ids(tracks)
users = get_users_by_id(session, user_id_list)
users = get_users_by_id(session, user_id_list, current_user_id)
for track in tracks:
user = users[track['owner_id']]
if user:
Expand Down
2 changes: 1 addition & 1 deletion discovery-provider/src/queries/get_trending_tracks.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def get_unpopulated_trending():

if args.get("with_users", False):
user_id_list = get_users_ids(sorted_tracks)
users = get_users_by_id(session, user_id_list)
users = get_users_by_id(session, user_id_list, current_user_id)
for track in sorted_tracks:
user = users[track['owner_id']]
if user:
Expand Down

0 comments on commit 97bb6d3

Please sign in to comment.