Skip to content

Commit eb47816

Browse files
authored
Merge pull request #139 from codeforjapan/feature/138
Search Mock
2 parents cb10fbd + 1cfd73b commit eb47816

File tree

3 files changed

+295
-5
lines changed

3 files changed

+295
-5
lines changed

api/birdxplorer_api/openapi_doc.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,98 @@ class FastAPIEndpointDocs(Generic[_KEY]):
496496
"participant_id": v1_data_user_enrollments_participant_id,
497497
},
498498
)
499+
500+
v1_data_x_user_name = FastAPIEndpointParamDocs(
501+
description="Xのユーザー名",
502+
openapi_examples={
503+
"single": {
504+
"summary": "@以降のユーザ名",
505+
"value": "elonmusk",
506+
},
507+
},
508+
)
509+
510+
v1_data_x_user_follower_count = FastAPIEndpointParamDocs(
511+
description="Xのユーザーのフォロワー数。",
512+
openapi_examples={
513+
"single": {
514+
"summary": "フォロワー数",
515+
"value": 100,
516+
},
517+
},
518+
)
519+
520+
v1_data_x_user_follow_count = FastAPIEndpointParamDocs(
521+
description="Xのユーザーのフォロー数。",
522+
openapi_examples={
523+
"single": {
524+
"summary": "フォロー数",
525+
"value": 100,
526+
},
527+
},
528+
)
529+
530+
v1_data_post_favorite_count = FastAPIEndpointParamDocs(
531+
description="Postのお気に入り数。",
532+
openapi_examples={
533+
"single": {
534+
"summary": "お気に入り数",
535+
"value": 100,
536+
},
537+
},
538+
)
539+
540+
v1_data_post_repost_count = FastAPIEndpointParamDocs(
541+
description="Postのリポスト数。",
542+
openapi_examples={
543+
"single": {
544+
"summary": "リポスト数",
545+
"value": 100,
546+
},
547+
},
548+
)
549+
550+
v1_data_post_impression_count = FastAPIEndpointParamDocs(
551+
description="Postのインプレッション数。",
552+
openapi_examples={
553+
"single": {
554+
"summary": "インプレッション数",
555+
"value": 100,
556+
},
557+
},
558+
)
559+
560+
v1_data_post_includes_media = FastAPIEndpointParamDocs(
561+
description="メディア情報を含んでいるか。",
562+
openapi_examples={
563+
"single": {
564+
"summary": "メディア情報を含める",
565+
"value": True,
566+
},
567+
},
568+
)
569+
570+
# Get /api/v1/data/search の OpenAPI ドキュメント
571+
V1DataSearchDocs = FastAPIEndpointDocs(
572+
"アドバンスドサーチでデータを取得するエンドポイント",
573+
{
574+
"note_includes_text": v1_data_notes_search_text,
575+
"note_excludes_text": v1_data_notes_search_text,
576+
"post_includes_text": v1_data_posts_search_text,
577+
"post_excludes_text": v1_data_posts_search_text,
578+
"language": v1_data_notes_language,
579+
"topic_ids": v1_date_notes_topic_ids,
580+
"note_status": v1_data_notes_current_status,
581+
"note_created_at_from": v1_data_notes_created_at_from,
582+
"note_created_at_to": v1_data_notes_created_at_to,
583+
"x_user_name": v1_data_x_user_name,
584+
"x_user_followers_count_from": v1_data_x_user_follower_count,
585+
"x_user_follow_count_from": v1_data_x_user_follow_count,
586+
"post_favorite_count_from": v1_data_post_favorite_count,
587+
"post_repost_count_from": v1_data_post_repost_count,
588+
"post_impression_count_from": v1_data_post_impression_count,
589+
"post_includes_media": v1_data_post_includes_media,
590+
"offset": v1_data_posts_offset,
591+
"limit": v1_data_posts_limit,
592+
},
593+
)

api/birdxplorer_api/routers/data.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from birdxplorer_api.openapi_doc import (
1111
V1DataNotesDocs,
1212
V1DataPostsDocs,
13+
V1DataSearchDocs,
1314
V1DataTopicsDocs,
1415
V1DataUserEnrollmentsDocs,
1516
)
@@ -22,6 +23,7 @@
2223
ParticipantId,
2324
Post,
2425
PostId,
26+
SummaryString,
2527
Topic,
2628
TopicId,
2729
TwitterTimestamp,
@@ -54,6 +56,18 @@
5456
),
5557
]
5658

59+
SearchPaginationMetaWithExamples: TypeAlias = Annotated[
60+
PaginationMeta,
61+
PydanticField(
62+
description="ページネーション用情報。 リクエスト時に指定した offset / limit の値に応じて、次のページや前のページのリクエスト用 URL が設定される。",
63+
json_schema_extra={
64+
"examples": [
65+
{"next": "http://birdxplorer.onrender.com/api/v1/data/search?offset=100&limit=100", "prev": "null"}
66+
]
67+
},
68+
),
69+
]
70+
5771
TopicListWithExamples: TypeAlias = Annotated[
5872
List[Topic],
5973
PydanticField(
@@ -141,6 +155,92 @@
141155
]
142156

143157

158+
class SearchedNote(BaseModel):
159+
noteId: NoteId
160+
summary: Annotated[SummaryString, PydanticField(description="コミュニティノートの本文")]
161+
language: Annotated[LanguageIdentifier, PydanticField(description="コミュニティノートの言語")]
162+
topics: Annotated[List[Topic], PydanticField(description="コミュニティノートに関連付けられたトピックのリスト")]
163+
postId: PostId
164+
current_status: Annotated[
165+
Annotated[
166+
str,
167+
PydanticField(
168+
json_schema_extra={
169+
"enum": ["NEEDS_MORE_RATINGS", "CURRENTLY_RATED_HELPFUL", "CURRENTLY_RATED_NOT_HELPFUL"]
170+
},
171+
),
172+
]
173+
| None,
174+
PydanticField(
175+
description="コミュニティノートの現在の評価状態",
176+
),
177+
]
178+
created_at: Annotated[
179+
TwitterTimestamp, PydanticField(description="コミュニティノートの作成日時 (ミリ秒単位の UNIX EPOCH TIMESTAMP)")
180+
]
181+
post: Annotated[Post, PydanticField(description="コミュニティノートに関連付けられた Post の情報")]
182+
183+
184+
SearchWithExamples: TypeAlias = Annotated[
185+
List[SearchedNote],
186+
PydanticField(
187+
description="検索結果のノートのリスト",
188+
json_schema_extra={
189+
"examples": [
190+
{
191+
"noteId": "1845672983001710655",
192+
"language": "ja",
193+
"topics": [
194+
{
195+
"topicId": 26,
196+
"label": {"ja": "セキュリティ上の脅威", "en": "security threat"},
197+
"referenceCount": 0,
198+
},
199+
{"topicId": 47, "label": {"ja": "検閲", "en": "Censorship"}, "referenceCount": 0},
200+
{"topicId": 51, "label": {"ja": "テクノロジー", "en": "technology"}, "referenceCount": 0},
201+
],
202+
"summary": "Content Security Policyは情報の持ち出しを防止する仕組みではありません。コンテンツインジェクションの脆弱性のリスクを軽減する仕組みです。適切なContent Security Policyがレスポンスヘッダーに設定されている場合でも、外部への通信をブロックできない点に注意が必要です。 Content Security Policy Level 3 https://w3c.github.io/webappsec-csp/", # noqa: E501
203+
"currentStatus": "NEEDS_MORE_RATINGS",
204+
"createdAt": 1728877704750,
205+
"post": {
206+
"postId": "1846718284369912064",
207+
"xUserId": "90954365",
208+
"xUser": {
209+
"userId": "90954365",
210+
"name": "earthquakejapan",
211+
"profileImage": "https://pbs.twimg.com/profile_images/1638600342/japan_rel96_normal.jpg",
212+
"followersCount": 162934,
213+
"followingCount": 6,
214+
},
215+
"text": "今後48時間以内に日本ではマグニチュード6.0の地震が発生する可能性があります。地図をご覧ください。(10月17日~10月18日) - https://t.co/nuyiVdM4FW https://t.co/Xd6U9XkpbL", # noqa: E501
216+
"mediaDetails": [
217+
{
218+
"mediaKey": "3_1846718279236177920-1846718284369912064",
219+
"type": "photo",
220+
"url": "https://pbs.twimg.com/media/GaDcfZoX0AAko2-.jpg",
221+
"width": 900,
222+
"height": 738,
223+
}
224+
],
225+
"createdAt": 1729094524000,
226+
"likeCount": 451,
227+
"repostCount": 104,
228+
"impressionCount": 82378,
229+
"links": [
230+
{
231+
"linkId": "9c139b99-8111-e4f0-ad41-fc9e40d08722",
232+
"url": "https://www.quakeprediction.com/Earthquake%20Forecast%20Japan.html",
233+
}
234+
],
235+
"link": "https://x.com/earthquakejapan/status/1846718284369912064",
236+
},
237+
},
238+
]
239+
},
240+
),
241+
]
242+
243+
144244
class TopicListResponse(BaseModel):
145245
data: TopicListWithExamples
146246

@@ -155,6 +255,11 @@ class PostListResponse(BaseModel):
155255
meta: PostsPaginationMetaWithExamples
156256

157257

258+
class SearchResponse(BaseModel):
259+
data: SearchWithExamples
260+
meta: SearchPaginationMetaWithExamples
261+
262+
158263
def str_to_twitter_timestamp(s: str) -> TwitterTimestamp:
159264
try:
160265
return TwitterTimestamp.from_int(int(s))
@@ -310,4 +415,94 @@ def get_posts(
310415

311416
return PostListResponse(data=posts, meta=PaginationMeta(next=next_url, prev=prev_url))
312417

418+
@router.get("/search", description=V1DataSearchDocs.description, response_model=SearchResponse)
419+
def search(
420+
note_includes_text: Union[None, str] = Query(default=None, **V1DataSearchDocs.params["note_includes_text"]),
421+
note_excludes_text: Union[None, str] = Query(default=None, **V1DataSearchDocs.params["note_excludes_text"]),
422+
post_includes_text: Union[None, str] = Query(default=None, **V1DataSearchDocs.params["post_includes_text"]),
423+
post_excludes_text: Union[None, str] = Query(default=None, **V1DataSearchDocs.params["post_excludes_text"]),
424+
language: Union[LanguageIdentifier, None] = Query(default=None, **V1DataSearchDocs.params["language"]),
425+
topic_ids: Union[List[TopicId], None] = Query(default=None, **V1DataSearchDocs.params["topic_ids"]),
426+
note_status: Union[None, List[str]] = Query(default=None, **V1DataSearchDocs.params["note_status"]),
427+
note_created_at_from: Union[None, TwitterTimestamp, str] = Query(
428+
default=None, **V1DataSearchDocs.params["note_created_at_from"]
429+
),
430+
note_created_at_to: Union[None, TwitterTimestamp, str] = Query(
431+
default=None, **V1DataSearchDocs.params["note_created_at_to"]
432+
),
433+
x_user_names: Union[List[str], None] = Query(default=None, **V1DataSearchDocs.params["x_user_name"]),
434+
x_user_followers_count_from: Union[None, int] = Query(
435+
default=None, **V1DataSearchDocs.params["x_user_followers_count_from"]
436+
),
437+
x_user_follow_count_from: Union[None, int] = Query(
438+
default=None, **V1DataSearchDocs.params["x_user_follow_count_from"]
439+
),
440+
post_favorite_count_from: Union[None, int] = Query(
441+
default=None, **V1DataSearchDocs.params["post_favorite_count_from"]
442+
),
443+
post_repost_count_from: Union[None, int] = Query(
444+
default=None, **V1DataSearchDocs.params["post_repost_count_from"]
445+
),
446+
post_impression_count_from: Union[None, int] = Query(
447+
default=None, **V1DataSearchDocs.params["post_impression_count_from"]
448+
),
449+
post_includes_media: bool = Query(default=True, **V1DataSearchDocs.params["post_includes_media"]),
450+
offset: int = Query(default=0, ge=0, **V1DataSearchDocs.params["offset"]),
451+
limit: int = Query(default=100, gt=0, le=1000, **V1DataSearchDocs.params["limit"]),
452+
) -> SearchResponse:
453+
return SearchResponse(
454+
data=[
455+
SearchedNote(
456+
noteId="1845672983001710655",
457+
language="ja",
458+
topics=[
459+
{
460+
"topicId": 26,
461+
"label": {"ja": "セキュリティ上の脅威", "en": "security threat"},
462+
"referenceCount": 0,
463+
},
464+
{"topicId": 47, "label": {"ja": "検閲", "en": "Censorship"}, "referenceCount": 0},
465+
{"topicId": 51, "label": {"ja": "テクノロジー", "en": "technology"}, "referenceCount": 0},
466+
],
467+
postId="1846718284369912064",
468+
summary="Content Security Policyは情報の持ち出しを防止する仕組みではありません。コンテンツインジェクションの脆弱性のリスクを軽減する仕組みです。適切なContent Security Policyがレスポンスヘッダーに設定されている場合でも、外部への通信をブロックできない点に注意が必要です。 Content Security Policy Level 3 https://w3c.github.io/webappsec-csp/", # noqa: E501
469+
current_status="NEEDS_MORE_RATINGS",
470+
created_at=1728877704750,
471+
post={
472+
"postId": "1846718284369912064",
473+
"xUserId": "90954365",
474+
"xUser": {
475+
"userId": "90954365",
476+
"name": "earthquakejapan",
477+
"profileImage": "https://pbs.twimg.com/profile_images/1638600342/japan_rel96_normal.jpg",
478+
"followersCount": 162934,
479+
"followingCount": 6,
480+
},
481+
"text": "今後48時間以内に日本ではマグニチュード6.0の地震が発生する可能性があります。地図をご覧ください。",
482+
"mediaDetails": [
483+
{
484+
"mediaKey": "3_1846718279236177920-1846718284369912064",
485+
"type": "photo",
486+
"url": "https://pbs.twimg.com/media/GaDcfZoX0AAko2-.jpg",
487+
"width": 900,
488+
"height": 738,
489+
}
490+
],
491+
"createdAt": 1729094524000,
492+
"likeCount": 451,
493+
"repostCount": 104,
494+
"impressionCount": 82378,
495+
"links": [
496+
{
497+
"linkId": "9c139b99-8111-e4f0-ad41-fc9e40d08722",
498+
"url": "https://www.quakeprediction.com/Earthquake%20Forecast%20Japan.html",
499+
}
500+
],
501+
"link": "https://x.com/earthquakejapan/status/1846718284369912064",
502+
},
503+
)
504+
],
505+
meta=PaginationMeta(next=None, prev=None),
506+
)
507+
313508
return router

common/birdxplorer_common/models.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from pydantic.alias_generators import to_camel
3131
from pydantic.json_schema import JsonSchemaValue
3232
from pydantic.main import IncEx
33-
from pydantic_core import Url, core_schema
33+
from pydantic_core import core_schema
3434

3535
StrT = TypeVar("StrT", bound="BaseString")
3636
IntT = TypeVar("IntT", bound="BaseInt")
@@ -799,11 +799,11 @@ class Link(BaseModel):
799799
t.co に短縮される前の URL ごとに一意な ID を持つ。
800800
801801
>>> Link.model_validate_json('{"linkId": "d5d15194-6574-0c01-8f6f-15abd72b2cf6", "url": "https://example.com"}')
802-
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=Url('https://example.com/'))
802+
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=HttpUrl('https://example.com/'))
803803
>>> Link(url="https://example.com/")
804-
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=Url('https://example.com/'))
804+
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=HttpUrl('https://example.com/'))
805805
>>> Link(link_id=UUID("d5d15194-6574-0c01-8f6f-15abd72b2cf6"), url="https://example.com/")
806-
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=Url('https://example.com/'))
806+
Link(link_id=LinkId('d5d15194-6574-0c01-8f6f-15abd72b2cf6'), url=HttpUrl('https://example.com/'))
807807
""" # noqa: E501
808808

809809
link_id: Annotated[LinkId, PydanticField(description="リンクを識別できる UUID")]
@@ -840,7 +840,7 @@ def link(self) -> HttpUrl:
840840
"""
841841
PostのX上でのURLを返す。
842842
"""
843-
return Url(f"https://x.com/{self.x_user.name}/status/{self.post_id}")
843+
return HttpUrl(f"https://x.com/{self.x_user.name}/status/{self.post_id}")
844844

845845

846846
class PaginationMeta(BaseModel):

0 commit comments

Comments
 (0)