server/api: add ability to select fields

This commit is contained in:
rr- 2016-05-30 23:08:22 +02:00
parent 8d1e23aa63
commit 037fbc61ec
15 changed files with 110 additions and 54 deletions

16
API.md
View File

@ -10,6 +10,7 @@
- [Basic requests](#basic-requests)
- [File uploads](#file-uploads)
- [Error handling](#error-handling)
- [Field selecting](#field-selecting)
2. [API reference](#api-reference)
@ -121,6 +122,21 @@ code together with JSON of following structure:
}
```
## Field selecting
For performance considerations, sometimes the client might want to choose the
fields the server sends to it in order to improve the query speed. This
customization is available for top-level fields of most of the
[resources](#resources). To choose the fields, the client should pass
`?_fields=field1,field2,...` suffix to the query. This works regardless of the
requesttype (`GET`, `PUT` etc.).
For example, to list posts while getting only their IDs and tags, the client
should send a `GET` query like this:
```
GET /posts/?_fields=id,tags
```
# API reference

View File

@ -1,7 +1,13 @@
import datetime
from szurubooru import search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, comments, posts, scores
from szurubooru.func import auth, comments, posts, scores, util
def _serialize(ctx, comment, **kwargs):
return comments.serialize_comment(
comment,
ctx.user,
options=util.get_serialization_options(ctx), **kwargs)
class CommentListApi(BaseApi):
def __init__(self):
@ -13,7 +19,7 @@ class CommentListApi(BaseApi):
auth.verify_privilege(ctx.user, 'comments:list')
return self._search_executor.execute_and_serialize(
ctx,
lambda comment: comments.serialize_comment(comment, ctx.user))
lambda comment: _serialize(ctx, comment))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'comments:create')
@ -23,13 +29,13 @@ class CommentListApi(BaseApi):
comment = comments.create_comment(ctx.user, post, text)
ctx.session.add(comment)
ctx.session.commit()
return comments.serialize_comment(comment, ctx.user)
return _serialize(ctx, comment)
class CommentDetailApi(BaseApi):
def get(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:view')
comment = comments.get_comment_by_id(comment_id)
return comments.serialize_comment(comment, ctx.user)
return _serialize(ctx, comment)
def put(self, ctx, comment_id):
comment = comments.get_comment_by_id(comment_id)
@ -39,7 +45,7 @@ class CommentDetailApi(BaseApi):
comment.last_edit_time = datetime.datetime.now()
comments.update_comment_text(comment, text)
ctx.session.commit()
return comments.serialize_comment(comment, ctx.user)
return _serialize(ctx, comment)
def delete(self, ctx, comment_id):
comment = comments.get_comment_by_id(comment_id)
@ -56,11 +62,11 @@ class CommentScoreApi(BaseApi):
comment = comments.get_comment_by_id(comment_id)
scores.set_score(comment, ctx.user, score)
ctx.session.commit()
return comments.serialize_comment(comment, ctx.user)
return _serialize(ctx, comment)
def delete(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:score')
comment = comments.get_comment_by_id(comment_id)
scores.delete_score(comment, ctx.user)
ctx.session.commit()
return comments.serialize_comment(comment, ctx.user)
return _serialize(ctx, comment)

View File

@ -9,6 +9,7 @@ class Context(object):
self.files = {}
self.input = {}
self.output = None
self.settings = {}
def has_param(self, name):
return name in self.input

View File

@ -1,7 +1,13 @@
import datetime
from szurubooru import search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, tags, posts, snapshots, favorites, scores
from szurubooru.func import auth, tags, posts, snapshots, favorites, scores, util
def _serialize_post(ctx, post):
return posts.serialize_post(
post,
ctx.user,
options=util.get_serialization_options(ctx))
class PostListApi(BaseApi):
def __init__(self):
@ -12,7 +18,7 @@ class PostListApi(BaseApi):
auth.verify_privilege(ctx.user, 'posts:list')
self._search_executor.config.user = ctx.user
return self._search_executor.execute_and_serialize(
ctx, lambda post: posts.serialize_post(post, ctx.user))
ctx, lambda post: _serialize_post(ctx, post))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'posts:create')
@ -38,13 +44,13 @@ class PostListApi(BaseApi):
snapshots.save_entity_creation(post, ctx.user)
ctx.session.commit()
tags.export_to_json()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
class PostDetailApi(BaseApi):
def get(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:view')
post = posts.get_post_by_id(post_id)
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
def put(self, ctx, post_id):
post = posts.get_post_by_id(post_id)
@ -79,7 +85,7 @@ class PostDetailApi(BaseApi):
snapshots.save_entity_modification(post, ctx.user)
ctx.session.commit()
tags.export_to_json()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
def delete(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:delete')
@ -104,11 +110,11 @@ class PostFeatureApi(BaseApi):
snapshots.save_entity_modification(featured_post, ctx.user)
snapshots.save_entity_modification(post, ctx.user)
ctx.session.commit()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
def get(self, ctx):
post = posts.try_get_featured_post()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
class PostScoreApi(BaseApi):
def put(self, ctx, post_id):
@ -117,14 +123,14 @@ class PostScoreApi(BaseApi):
score = ctx.get_param_as_int('score', required=True)
scores.set_score(post, ctx.user, score)
ctx.session.commit()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
def delete(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:score')
post = posts.get_post_by_id(post_id)
scores.delete_score(post, ctx.user)
ctx.session.commit()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
class PostFavoriteApi(BaseApi):
def post(self, ctx, post_id):
@ -132,11 +138,11 @@ class PostFavoriteApi(BaseApi):
post = posts.get_post_by_id(post_id)
favorites.set_favorite(post, ctx.user)
ctx.session.commit()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)
def delete(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:favorite')
post = posts.get_post_by_id(post_id)
favorites.unset_favorite(post, ctx.user)
ctx.session.commit()
return posts.serialize_post(post, ctx.user)
return _serialize_post(ctx, post)

View File

@ -1,7 +1,11 @@
import datetime
from szurubooru import db, search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, tags, snapshots
from szurubooru.func import auth, tags, util, snapshots
def _serialize(ctx, tag):
return tags.serialize_tag(
tag, options=util.get_serialization_options(ctx))
def _create_if_needed(tag_names, user):
if not tag_names:
@ -20,7 +24,7 @@ class TagListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'tags:list')
return self._search_executor.execute_and_serialize(
ctx, tags.serialize_tag)
ctx, lambda tag: _serialize(ctx, tag))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'tags:create')
@ -41,13 +45,13 @@ class TagListApi(BaseApi):
snapshots.save_entity_creation(tag, ctx.user)
ctx.session.commit()
tags.export_to_json()
return tags.serialize_tag(tag)
return _serialize(ctx, tag)
class TagDetailApi(BaseApi):
def get(self, ctx, tag_name):
auth.verify_privilege(ctx.user, 'tags:view')
tag = tags.get_tag_by_name(tag_name)
return tags.serialize_tag(tag)
return _serialize(ctx, tag)
def put(self, ctx, tag_name):
tag = tags.get_tag_by_name(tag_name)
@ -73,7 +77,7 @@ class TagDetailApi(BaseApi):
snapshots.save_entity_modification(tag, ctx.user)
ctx.session.commit()
tags.export_to_json()
return tags.serialize_tag(tag)
return _serialize(ctx, tag)
def delete(self, ctx, tag_name):
tag = tags.get_tag_by_name(tag_name)
@ -101,7 +105,7 @@ class TagMergeApi(BaseApi):
tags.merge_tags(source_tag, target_tag)
ctx.session.commit()
tags.export_to_json()
return tags.serialize_tag(target_tag)
return _serialize(ctx, target_tag)
class TagSiblingsApi(BaseApi):
def get(self, ctx, tag_name):
@ -111,7 +115,7 @@ class TagSiblingsApi(BaseApi):
serialized_siblings = []
for sibling, occurrences in result:
serialized_siblings.append({
'tag': tags.serialize_tag(sibling),
'tag': _serialize(ctx, sibling),
'occurrences': occurrences
})
return {'results': serialized_siblings}

View File

@ -1,14 +1,16 @@
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, tags, tag_categories, snapshots
from szurubooru.func import auth, tags, tag_categories, util, snapshots
def _serialize(ctx, category):
return tag_categories.serialize_category(
category, options=util.get_serialization_options(ctx))
class TagCategoryListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'tag_categories:list')
categories = tag_categories.get_all_categories()
return {
'results': [
tag_categories.serialize_category(category) \
for category in categories],
'results': [_serialize(ctx, category) for category in categories],
}
def post(self, ctx):
@ -21,13 +23,13 @@ class TagCategoryListApi(BaseApi):
snapshots.save_entity_creation(category, ctx.user)
ctx.session.commit()
tags.export_to_json()
return tag_categories.serialize_category(category)
return _serialize(ctx, category)
class TagCategoryDetailApi(BaseApi):
def get(self, ctx, category_name):
auth.verify_privilege(ctx.user, 'tag_categories:view')
category = tag_categories.get_category_by_name(category_name)
return tag_categories.serialize_category(category)
return _serialize(ctx, category)
def put(self, ctx, category_name):
category = tag_categories.get_category_by_name(category_name)
@ -43,7 +45,7 @@ class TagCategoryDetailApi(BaseApi):
snapshots.save_entity_modification(category, ctx.user)
ctx.session.commit()
tags.export_to_json()
return tag_categories.serialize_category(category)
return _serialize(ctx, category)
def delete(self, ctx, category_name):
category = tag_categories.get_category_by_name(category_name)
@ -69,4 +71,4 @@ class DefaultTagCategoryApi(BaseApi):
snapshots.save_entity_modification(category, ctx.user)
ctx.session.commit()
tags.export_to_json()
return tag_categories.serialize_category(category)
return _serialize(ctx, category)

View File

@ -1,6 +1,13 @@
from szurubooru import search
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, users
from szurubooru.func import auth, users, util
def _serialize(ctx, user, **kwargs):
return users.serialize_user(
user,
ctx.user,
options=util.get_serialization_options(ctx),
**kwargs)
class UserListApi(BaseApi):
def __init__(self):
@ -10,7 +17,7 @@ class UserListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'users:list')
return self._search_executor.execute_and_serialize(
ctx, lambda user: users.serialize_user(user, ctx.user))
ctx, lambda user: _serialize(ctx, user))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'users:create')
@ -28,13 +35,13 @@ class UserListApi(BaseApi):
ctx.get_file('avatar'))
ctx.session.add(user)
ctx.session.commit()
return users.serialize_user(user, ctx.user, force_show_email=True)
return _serialize(ctx, user, force_show_email=True)
class UserDetailApi(BaseApi):
def get(self, ctx, user_name):
auth.verify_privilege(ctx.user, 'users:view')
user = users.get_user_by_name(user_name)
return users.serialize_user(user, ctx.user)
return _serialize(ctx, user)
def put(self, ctx, user_name):
user = users.get_user_by_name(user_name)
@ -60,7 +67,7 @@ class UserDetailApi(BaseApi):
ctx.get_param_as_string('avatarStyle'),
ctx.get_file('avatar'))
ctx.session.commit()
return users.serialize_user(user, ctx.user)
return _serialize(ctx, user)
def delete(self, ctx, user_name):
user = users.get_user_by_name(user_name)

View File

@ -5,7 +5,7 @@ from szurubooru.func import users, scores, util
class CommentNotFoundError(errors.NotFoundError): pass
class EmptyCommentTextError(errors.ValidationError): pass
def serialize_comment(comment, authenticated_user):
def serialize_comment(comment, authenticated_user, options=None):
return util.serialize_entity(
comment,
{
@ -16,7 +16,8 @@ def serialize_comment(comment, authenticated_user):
'creationTime': lambda: comment.creation_time,
'lastEditTime': lambda: comment.last_edit_time,
'ownScore': lambda: scores.get_score(comment, authenticated_user),
})
},
options)
def try_get_comment_by_id(comment_id):
return db.session \

View File

@ -61,7 +61,7 @@ def serialize_note(note):
'text': note.text,
}
def serialize_post(post, authenticated_user):
def serialize_post(post, authenticated_user, options=None):
return util.serialize_entity(
post,
{
@ -98,7 +98,8 @@ def serialize_post(post, authenticated_user):
comments.serialize_comment(comment, authenticated_user) \
for comment in post.comments],
'snapshots': lambda: snapshots.get_serialized_history(post),
})
},
options)
def get_post_count():
return db.session.query(sqlalchemy.func.count(db.Post.post_id)).one()[0]

View File

@ -14,7 +14,7 @@ def _verify_name_validity(name):
raise InvalidTagCategoryNameError(
'Name must satisfy regex %r.' % name_regex)
def serialize_category(category):
def serialize_category(category, options=None):
return util.serialize_entity(
category,
{
@ -23,7 +23,8 @@ def serialize_category(category):
'usages': lambda: category.tag_count,
'default': lambda: category.default,
'snapshots': lambda: snapshots.get_serialized_history(category),
})
},
options)
def create_category(name, color):
category = db.TagCategory()

View File

@ -36,7 +36,7 @@ def _get_default_category_name():
else:
return DEFAULT_CATEGORY_NAME
def serialize_tag(tag):
def serialize_tag(tag, options=None):
return util.serialize_entity(
tag,
{
@ -50,7 +50,8 @@ def serialize_tag(tag):
'implications': lambda: [
relation.names[0].name for relation in tag.implications],
'snapshots': lambda: snapshots.get_serialized_history(tag),
})
},
options)
def export_to_json():
output = {

View File

@ -28,7 +28,7 @@ def _get_email(user, authenticated_user, force_show_email):
return False
return user.email
def serialize_user(user, authenticated_user, force_show_email=False):
def serialize_user(user, authenticated_user, options=None, force_show_email=False):
return util.serialize_entity(
user,
{
@ -39,7 +39,8 @@ def serialize_user(user, authenticated_user, force_show_email=False):
'avatarStyle': lambda: user.avatar_style,
'avatarUrl': lambda: _get_avatar_url(user),
'email': lambda: _get_email(user, authenticated_user, force_show_email),
})
},
options)
def get_user_count():
return db.session.query(db.User).count()

View File

@ -6,12 +6,21 @@ import tempfile
from contextlib import contextmanager
from szurubooru.errors import ValidationError
def serialize_entity(entity, field_factories):
def get_serialization_options(ctx):
return ctx.get_param_as_list('_fields', required=False, default=None)
def serialize_entity(entity, field_factories, options):
if not entity:
return None
if not options:
options = field_factories.keys()
ret = {}
for key, factory in field_factories.items():
ret[key] = factory()
for key in options:
try:
factory = field_factories[key]
ret[key] = factory()
except KeyError:
pass
return ret
@contextmanager

View File

@ -54,7 +54,7 @@ def test_creating_minimal_posts(
posts.update_post_notes.assert_called_once_with(post, [])
posts.update_post_flags.assert_called_once_with(post, [])
posts.update_post_thumbnail.assert_called_once_with(post, 'post-thumbnail')
posts.serialize_post.assert_called_once_with(post, auth_user)
posts.serialize_post.assert_called_once_with(post, auth_user, options=None)
tags.export_to_json.assert_called_once_with()
snapshots.save_entity_creation.assert_called_once_with(post, auth_user)
@ -100,7 +100,7 @@ def test_creating_full_posts(context_factory, post_factory, user_factory):
posts.update_post_relations.assert_called_once_with(post, [1, 2])
posts.update_post_notes.assert_called_once_with(post, ['note1', 'note2'])
posts.update_post_flags.assert_called_once_with(post, ['flag1', 'flag2'])
posts.serialize_post.assert_called_once_with(post, auth_user)
posts.serialize_post.assert_called_once_with(post, auth_user, options=None)
tags.export_to_json.assert_called_once_with()
snapshots.save_entity_creation.assert_called_once_with(post, auth_user)

View File

@ -67,7 +67,7 @@ def test_post_updating(
posts.update_post_relations.assert_called_once_with(post, [1, 2])
posts.update_post_notes.assert_called_once_with(post, ['note1', 'note2'])
posts.update_post_flags.assert_called_once_with(post, ['flag1', 'flag2'])
posts.serialize_post.assert_called_once_with(post, auth_user)
posts.serialize_post.assert_called_once_with(post, auth_user, options=None)
tags.export_to_json.assert_called_once_with()
snapshots.save_entity_modification.assert_called_once_with(post, auth_user)
assert post.last_edit_time == datetime.datetime(1997, 1, 1)