server/api: add ability to select fields
This commit is contained in:
parent
8d1e23aa63
commit
037fbc61ec
16
API.md
16
API.md
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue