diff --git a/API.md b/API.md index 8d4eccd8..da2edd68 100644 --- a/API.md +++ b/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 diff --git a/server/szurubooru/api/comment_api.py b/server/szurubooru/api/comment_api.py index 8aeaceca..e25ce606 100644 --- a/server/szurubooru/api/comment_api.py +++ b/server/szurubooru/api/comment_api.py @@ -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) diff --git a/server/szurubooru/api/context.py b/server/szurubooru/api/context.py index b1da7287..93db39f7 100644 --- a/server/szurubooru/api/context.py +++ b/server/szurubooru/api/context.py @@ -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 diff --git a/server/szurubooru/api/post_api.py b/server/szurubooru/api/post_api.py index 3f0eb941..1496952d 100644 --- a/server/szurubooru/api/post_api.py +++ b/server/szurubooru/api/post_api.py @@ -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) diff --git a/server/szurubooru/api/tag_api.py b/server/szurubooru/api/tag_api.py index 65cecf90..372db1a9 100644 --- a/server/szurubooru/api/tag_api.py +++ b/server/szurubooru/api/tag_api.py @@ -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} diff --git a/server/szurubooru/api/tag_category_api.py b/server/szurubooru/api/tag_category_api.py index ffae5e0c..04b4fa18 100644 --- a/server/szurubooru/api/tag_category_api.py +++ b/server/szurubooru/api/tag_category_api.py @@ -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) diff --git a/server/szurubooru/api/user_api.py b/server/szurubooru/api/user_api.py index 29371ce6..b7d5bd00 100644 --- a/server/szurubooru/api/user_api.py +++ b/server/szurubooru/api/user_api.py @@ -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) diff --git a/server/szurubooru/func/comments.py b/server/szurubooru/func/comments.py index 8ea06e3d..f6e18e97 100644 --- a/server/szurubooru/func/comments.py +++ b/server/szurubooru/func/comments.py @@ -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 \ diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 08859b86..99ff36d2 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -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] diff --git a/server/szurubooru/func/tag_categories.py b/server/szurubooru/func/tag_categories.py index 6d3f8f79..5e91c481 100644 --- a/server/szurubooru/func/tag_categories.py +++ b/server/szurubooru/func/tag_categories.py @@ -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() diff --git a/server/szurubooru/func/tags.py b/server/szurubooru/func/tags.py index 9f07cddd..057fa18e 100644 --- a/server/szurubooru/func/tags.py +++ b/server/szurubooru/func/tags.py @@ -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 = { diff --git a/server/szurubooru/func/users.py b/server/szurubooru/func/users.py index 128e6264..d2f46c52 100644 --- a/server/szurubooru/func/users.py +++ b/server/szurubooru/func/users.py @@ -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() diff --git a/server/szurubooru/func/util.py b/server/szurubooru/func/util.py index 3a4208a8..908c760f 100644 --- a/server/szurubooru/func/util.py +++ b/server/szurubooru/func/util.py @@ -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 diff --git a/server/szurubooru/tests/api/test_post_creating.py b/server/szurubooru/tests/api/test_post_creating.py index adb0b208..a15efbc6 100644 --- a/server/szurubooru/tests/api/test_post_creating.py +++ b/server/szurubooru/tests/api/test_post_creating.py @@ -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) diff --git a/server/szurubooru/tests/api/test_post_updating.py b/server/szurubooru/tests/api/test_post_updating.py index fc14c275..f2852652 100644 --- a/server/szurubooru/tests/api/test_post_updating.py +++ b/server/szurubooru/tests/api/test_post_updating.py @@ -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)