From 441cfdbbc18ca234710082b3f8cdd2513b5d7dc2 Mon Sep 17 00:00:00 2001 From: jasta Date: Sat, 17 Aug 2024 14:51:38 +0900 Subject: [PATCH 1/4] add pagenation meta class --- api/birdxplorer_api/routers/data.py | 10 ++++++---- common/birdxplorer_common/models.py | 6 +++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index ee6f1a7..f5ea26c 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -16,6 +16,7 @@ TopicId, TwitterTimestamp, UserEnrollment, + PagenationMeta ) from birdxplorer_common.storage import Storage @@ -30,6 +31,7 @@ class NoteListResponse(BaseModel): class PostListResponse(BaseModel): data: List[Post] + meta: PagenationMeta def str_to_twitter_timestamp(s: str) -> TwitterTimestamp: @@ -133,10 +135,10 @@ def get_posts( return PostListResponse( data=paginated_posts, - meta={ - "next": next_url, - "prev": prev_url - } + meta=PagenationMeta( + next=next_url, + prev=prev_url + ) ) return router diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 7a1803a..afb876a 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone from enum import Enum -from typing import Any, Dict, List, Literal, Type, TypeAlias, TypeVar, Union +from typing import Any, Dict, List, Literal, Type, TypeAlias, TypeVar, Union, Optional from pydantic import BaseModel as PydanticBaseModel from pydantic import ConfigDict, GetCoreSchemaHandler, HttpUrl, TypeAdapter @@ -672,3 +672,7 @@ class Post(BaseModel): like_count: NonNegativeInt repost_count: NonNegativeInt impression_count: NonNegativeInt + +class PagenationMeta(BaseModel): + next: Optional[HttpUrl] = None + prev: Optional[HttpUrl] = None \ No newline at end of file From 65d141c472ba47420e958470bcafeb20915093a4 Mon Sep 17 00:00:00 2001 From: jasta Date: Sat, 17 Aug 2024 14:54:43 +0900 Subject: [PATCH 2/4] fix typo --- api/birdxplorer_api/routers/data.py | 8 ++++---- common/birdxplorer_common/models.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index f5ea26c..3d3b81c 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -16,7 +16,7 @@ TopicId, TwitterTimestamp, UserEnrollment, - PagenationMeta + PaginationMeta ) from birdxplorer_common.storage import Storage @@ -97,8 +97,8 @@ def get_posts( note_id: Union[List[NoteId], None] = Query(default=None), created_at_start: Union[None, TwitterTimestamp, str] = Query(default=None), created_at_end: Union[None, TwitterTimestamp, str] = Query(default=None), - offset: int = Query(default=0, ge=0), # 確保 offset 是非負的 - limit: int = Query(default=100, gt=0, le=1000) # 確保 limit 在合理範圍內 + offset: int = Query(default=0, ge=0), + limit: int = Query(default=100, gt=0, le=1000) ) -> PostListResponse: posts = None @@ -135,7 +135,7 @@ def get_posts( return PostListResponse( data=paginated_posts, - meta=PagenationMeta( + meta=PaginationMeta( next=next_url, prev=prev_url ) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index afb876a..3f8df05 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -673,6 +673,6 @@ class Post(BaseModel): repost_count: NonNegativeInt impression_count: NonNegativeInt -class PagenationMeta(BaseModel): +class PaginationMeta(BaseModel): next: Optional[HttpUrl] = None prev: Optional[HttpUrl] = None \ No newline at end of file From 6d2c98083baa50a81998324439e90300534671db Mon Sep 17 00:00:00 2001 From: jasta Date: Sat, 17 Aug 2024 15:27:21 +0900 Subject: [PATCH 3/4] pagination feature done --- api/birdxplorer_api/routers/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index 3d3b81c..fade616 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -31,7 +31,7 @@ class NoteListResponse(BaseModel): class PostListResponse(BaseModel): data: List[Post] - meta: PagenationMeta + meta: PaginationMeta def str_to_twitter_timestamp(s: str) -> TwitterTimestamp: From 94c6cb91054f77c6531a1c4d218c9272a86379dc Mon Sep 17 00:00:00 2001 From: yu23ki14 Date: Sat, 17 Aug 2024 15:32:45 +0900 Subject: [PATCH 4/4] test for navigation --- api/birdxplorer_api/routers/data.py | 16 +++++--------- api/tests/routers/test_data.py | 34 +++++++++++++++++++++-------- common/birdxplorer_common/models.py | 5 +++-- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index fade616..418cbd6 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -9,6 +9,7 @@ LanguageIdentifier, Note, NoteId, + PaginationMeta, ParticipantId, Post, PostId, @@ -16,7 +17,6 @@ TopicId, TwitterTimestamp, UserEnrollment, - PaginationMeta ) from birdxplorer_common.storage import Storage @@ -98,7 +98,7 @@ def get_posts( created_at_start: Union[None, TwitterTimestamp, str] = Query(default=None), created_at_end: Union[None, TwitterTimestamp, str] = Query(default=None), offset: int = Query(default=0, ge=0), - limit: int = Query(default=100, gt=0, le=1000) + limit: int = Query(default=100, gt=0, le=1000), ) -> PostListResponse: posts = None @@ -122,8 +122,8 @@ def get_posts( posts = list(storage.get_posts()) total_count = len(posts) - paginated_posts = posts[offset:offset + limit] - base_url = str(request.url).split('?')[0] + paginated_posts = posts[offset : offset + limit] + base_url = str(request.url).split("?")[0] next_offset = offset + limit prev_offset = max(offset - limit, 0) next_url = None @@ -133,12 +133,6 @@ def get_posts( if offset > 0: prev_url = f"{base_url}?offset={prev_offset}&limit={limit}" - return PostListResponse( - data=paginated_posts, - meta=PaginationMeta( - next=next_url, - prev=prev_url - ) - ) + return PostListResponse(data=paginated_posts, meta=PaginationMeta(next=next_url, prev=prev_url)) return router diff --git a/api/tests/routers/test_data.py b/api/tests/routers/test_data.py index ddd29ad..86356ab 100644 --- a/api/tests/routers/test_data.py +++ b/api/tests/routers/test_data.py @@ -24,7 +24,10 @@ def test_posts_get(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(d.model_dump_json()) for d in post_samples]} + assert res_json == { + "data": [json.loads(d.model_dump_json()) for d in post_samples], + "meta": {"next": None, "prev": None}, + } def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Post]) -> None: @@ -35,7 +38,8 @@ def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Pos "data": [ json.loads(post_samples[0].model_dump_json()), json.loads(post_samples[2].model_dump_json()), - ] + ], + "meta": {"next": None, "prev": None}, } @@ -43,49 +47,61 @@ def test_posts_get_has_note_id_filter(client: TestClient, post_samples: List[Pos response = client.get(f"/api/v1/data/posts/?noteId={','.join([n.note_id for n in note_samples])}") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[0].model_dump_json())]} + assert res_json == {"data": [json.loads(post_samples[0].model_dump_json())], "meta": {"next": None, "prev": None}} def test_posts_get_has_created_at_filter_start_and_end(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=2006-7-25 00:00:00&createdAtEnd=2006-7-30 23:59:59") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())]} + assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())], "meta": {"next": None, "prev": None}} def test_posts_get_has_created_at_filter_start(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=2006-7-25 00:00:00") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_has_created_at_filter_end(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtEnd=2006-7-30 00:00:00") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_created_at_range_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=1153921700000&createdAtEnd=1154921800000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())]} + assert res_json == {"data": [json.loads(post_samples[1].model_dump_json())], "meta": {"next": None, "prev": None}} def test_posts_get_created_at_start_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtStart=1153921700000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (1, 2)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_created_at_end_filter_accepts_integer(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?createdAtEnd=1154921800000") assert response.status_code == 200 res_json = response.json() - assert res_json == {"data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)]} + assert res_json == { + "data": [json.loads(post_samples[i].model_dump_json()) for i in (0, 1)], + "meta": {"next": None, "prev": None}, + } def test_posts_get_timestamp_out_of_range(client: TestClient, post_samples: List[Post]) -> None: diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 3f8df05..b5c2f87 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timezone from enum import Enum -from typing import Any, Dict, List, Literal, Type, TypeAlias, TypeVar, Union, Optional +from typing import Any, Dict, List, Literal, Optional, Type, TypeAlias, TypeVar, Union from pydantic import BaseModel as PydanticBaseModel from pydantic import ConfigDict, GetCoreSchemaHandler, HttpUrl, TypeAdapter @@ -673,6 +673,7 @@ class Post(BaseModel): repost_count: NonNegativeInt impression_count: NonNegativeInt + class PaginationMeta(BaseModel): next: Optional[HttpUrl] = None - prev: Optional[HttpUrl] = None \ No newline at end of file + prev: Optional[HttpUrl] = None