diff --git a/README.md b/README.md index 97c0c4c..78fdee3 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,10 @@ BirdXplorer is software that helps users explore community notes data on X (formerly known as Twitter). +## Example Usecase + +See [example](./docs/example.md) + ## Development ### Requirements diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index 415a8b0..8dbde93 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -250,8 +250,8 @@ def get_notes( @router.get("/posts", description=V1DataPostsDocs.description, response_model=PostListResponse) def get_posts( request: Request, - post_id: Union[List[PostId], None] = Query(default=None, **V1DataPostsDocs.params["post_id"]), - note_id: Union[List[NoteId], None] = Query(default=None, **V1DataPostsDocs.params["note_id"]), + post_ids: Union[List[PostId], None] = Query(default=None), + note_ids: Union[List[NoteId], None] = Query(default=None), created_at_from: Union[None, TwitterTimestamp, str] = Query( default=None, **V1DataPostsDocs.params["created_at_from"] ), @@ -270,8 +270,8 @@ def get_posts( created_at_to = ensure_twitter_timestamp(created_at_to) posts = list( storage.get_posts( - post_ids=post_id, - note_ids=note_id, + post_ids=post_ids, + note_ids=note_ids, start=created_at_from, end=created_at_to, search_text=search_text, @@ -283,8 +283,8 @@ def get_posts( ) total_count = storage.get_number_of_posts( - post_ids=post_id, - note_ids=note_id, + post_ids=post_ids, + note_ids=note_ids, start=created_at_from, end=created_at_to, search_text=search_text, diff --git a/api/tests/routers/test_data.py b/api/tests/routers/test_data.py index c9dfbff..66cdb74 100644 --- a/api/tests/routers/test_data.py +++ b/api/tests/routers/test_data.py @@ -44,7 +44,7 @@ def test_posts_get_limit_and_offset(client: TestClient, post_samples: List[Post] def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Post]) -> None: - response = client.get(f"/api/v1/data/posts/?postId={post_samples[0].post_id},{post_samples[2].post_id}") + response = client.get(f"/api/v1/data/posts/?postIds={post_samples[0].post_id},{post_samples[2].post_id}") assert response.status_code == 200 res_json = response.json() assert res_json == { @@ -57,7 +57,7 @@ def test_posts_get_has_post_id_filter(client: TestClient, post_samples: List[Pos def test_posts_get_has_note_id_filter(client: TestClient, post_samples: List[Post], note_samples: List[Note]) -> None: - response = client.get(f"/api/v1/data/posts/?noteId={','.join([n.note_id for n in note_samples])}") + response = client.get(f"/api/v1/data/posts/?noteIds={','.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())], "meta": {"next": None, "prev": None}} @@ -123,7 +123,7 @@ def test_posts_get_timestamp_out_of_range(client: TestClient, post_samples: List def test_posts_get_with_media_by_default(client: TestClient, post_samples: List[Post]) -> None: - response = client.get("/api/v1/data/posts/?postId=2234567890123456791") + response = client.get("/api/v1/data/posts/?postIds=2234567890123456791") assert response.status_code == 200 res_json_default = response.json() @@ -134,7 +134,7 @@ def test_posts_get_with_media_by_default(client: TestClient, post_samples: List[ def test_posts_get_with_media_true(client: TestClient, post_samples: List[Post]) -> None: - response = client.get("/api/v1/data/posts/?postId=2234567890123456791&media=true") + response = client.get("/api/v1/data/posts/?postIds=2234567890123456791&media=true") assert response.status_code == 200 res_json_default = response.json() @@ -146,7 +146,7 @@ def test_posts_get_with_media_true(client: TestClient, post_samples: List[Post]) def test_posts_get_with_media_false(client: TestClient, post_samples: List[Post]) -> None: expected_post = post_samples[1].model_copy(update={"media_details": []}) - response = client.get("/api/v1/data/posts/?postId=2234567890123456791&media=false") + response = client.get("/api/v1/data/posts/?postIds=2234567890123456791&media=false") assert response.status_code == 200 res_json_default = response.json() diff --git a/docs/example.md b/docs/example.md new file mode 100644 index 0000000..ad98304 --- /dev/null +++ b/docs/example.md @@ -0,0 +1,50 @@ +# BirdXplorer の 使用例 + +## API仕様の閲覧 + +API 仕様は、[Swagger UI](https://birdxplorer.onrender.com/docs) で閲覧できます。 + +また、[OpenAPI Spec](https://birdxplorer.onrender.com/openapi.json) も提供しています。 + +> [!TIP] +> OpenAPI Specification から API リクエスト用のコードを生成するライブラリを使用することで、 +> API の入出力をコード上で安全に扱えることがあります。 + +## 特定のトピックのコミュニティノートと、そのトピックに関連するツイートを取得する + +BirdXplorer では、コミュニティノートのトピックを AI で推定して分類しています。 +この分類の候補は、 `/api/v1/data/topics` で取得できます。 + +ここでは、トピック: テクノロジー (topicId: 51) について、そのコミュニティノート500件とコミュニティノートに関連するツイートを取得する例を示します。 + +```python +#!python3.10 +import json + +import requests + +# AI で推定 / 分類した際に 「テクノロジー」 と判定されたコミュニティノートを取得するための id +# その他の種類は `https://birdxplorer.onrender.com/api/v1/data/topics` で取得できます +TECHNOLOGY_TOPIC_ID = 51 + +offset = 0 +expected_data_amount = 500 # 最大で 1000 まで指定できます + +tech_notes_res = requests.get( + f"https://birdxplorer.onrender.com/api/v1/data/notes?offset={offset}&limit={expected_data_amount}&topic_ids={TECHNOLOGY_TOPIC_ID}&language=ja" +) +tech_notes = tech_notes_res.json()["data"] + +# コミュニティノート と X の Post は 1:1 で対応しています +tech_post_ids = list(map(lambda x: x["postId"], tech_notes)) +post_ids = ",".join(tech_post_ids) + +posts_res = requests.get( + f"https://birdxplorer.onrender.com/api/v1/data/posts?post_ids={post_ids}&limit={expected_data_amount}" +) +tech_posts = posts_res.json()["data"] + + +with open("tech_posts.json", "w") as f: + f.write(json.dumps(tech_posts, ensure_ascii=False, indent=2)) +```