From d4e51453eececcea4c0c38b93e49c5cf19e8c55e Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 19:33:09 +0900 Subject: [PATCH 01/14] =?UTF-8?q?MediaDetails=E3=82=92=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/models.py | 34 ++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index ec33593..b1a7f1b 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -2,7 +2,18 @@ from datetime import datetime, timezone from enum import Enum from random import Random -from typing import Any, Dict, List, Literal, Optional, Type, TypeAlias, TypeVar, Union +from typing import ( + Annotated, + Any, + Dict, + List, + Literal, + Optional, + Type, + TypeAlias, + TypeVar, + Union, +) from uuid import UUID from pydantic import BaseModel as PydanticBaseModel @@ -13,11 +24,9 @@ TypeAdapter, model_validator, ) -from pydantic.alias_generators import to_camel -from pydantic.main import IncEx +from pydantic import Field as PydanticField from pydantic_core import core_schema -StrT = TypeVar("StrT", bound="BaseString") IntT = TypeVar("IntT", bound="BaseInt") FloatT = TypeVar("FloatT", bound="BaseFloat") @@ -683,7 +692,20 @@ class XUser(BaseModel): following_count: NonNegativeInt -MediaDetails: TypeAlias = List[HttpUrl] | None +# ref: https://developer.x.com/en/docs/x-api/data-dictionary/object-model/media +XMediaType: TypeAlias = Literal["photo", "video", "animated_gif"] + + +class XMedia(BaseModel): + media_key: str + + type: XMediaType + url: HttpUrl + width: NonNegativeInt + height: NonNegativeInt + + +MediaDetails: TypeAlias = list[XMedia] | None class LinkId(UUID): @@ -754,7 +776,7 @@ class Post(BaseModel): x_user_id: UserId x_user: XUser text: str - media_details: MediaDetails = None + media_details: Annotated[MediaDetails, PydanticField(default_factory=lambda: None)] created_at: TwitterTimestamp like_count: NonNegativeInt repost_count: NonNegativeInt From 5693e9b1d0169472ebd430e61c7e97b658eaa1c6 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 19:33:26 +0900 Subject: [PATCH 02/14] =?UTF-8?q?MediaDetails=E3=82=92DB=E5=81=B4=E3=81=A7?= =?UTF-8?q?=E8=A1=A8=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/storage.py | 79 +++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 2b7868b..0645163 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -7,22 +7,31 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from sqlalchemy.types import CHAR, DECIMAL, JSON, Integer, String, Uuid -from .models import BinaryBool, LanguageIdentifier -from .models import Link as LinkModel -from .models import LinkId, MediaDetails, NonNegativeInt -from .models import Note as NoteModel -from .models import NoteId, NotesClassification, NotesHarmful, ParticipantId -from .models import Post as PostModel -from .models import PostId, SummaryString -from .models import Topic as TopicModel from .models import ( + BinaryBool, + LanguageIdentifier, + LinkId, + MediaDetails, + NonNegativeInt, + NoteId, + NotesClassification, + NotesHarmful, + ParticipantId, + PostId, + SummaryString, TopicId, TopicLabel, TwitterTimestamp, UserEnrollment, UserId, UserName, + XMedia, + XMediaType, ) +from .models import Link as LinkModel +from .models import Note as NoteModel +from .models import Post as PostModel +from .models import Topic as TopicModel from .models import XUser as XUserModel from .settings import GlobalSettings @@ -107,6 +116,29 @@ class PostLinkAssociation(Base): link: Mapped[LinkRecord] = relationship() +class PostMediaAssociation(Base): + __tablename__ = "post_media" + + post_id: Mapped[PostId] = mapped_column(ForeignKey("posts.post_id"), primary_key=True) + media_key: Mapped[str] = mapped_column(ForeignKey("x_medias.media_key"), primary_key=True) + + # このテーブルにアクセスした時点でほぼ間違いなく MediaRecord も必要なので一気に引っ張る + media: Mapped["MediaRecord"] = relationship(back_populates="post_media_association", lazy="joined") + + +class MediaRecord(Base): + __tablename__ = "x_medias" + + media_key: Mapped[str] = mapped_column(primary_key=True) + + type: Mapped[XMediaType] = mapped_column(nullable=False) + url: Mapped[HttpUrl] = mapped_column(nullable=False) + width: Mapped[NonNegativeInt] = mapped_column(nullable=False) + height: Mapped[NonNegativeInt] = mapped_column(nullable=False) + + post_media_association: Mapped["PostMediaAssociation"] = relationship(back_populates="media") + + class PostRecord(Base): __tablename__ = "posts" @@ -114,7 +146,7 @@ class PostRecord(Base): user_id: Mapped[UserId] = mapped_column(ForeignKey("x_users.user_id"), nullable=False) user: Mapped[XUserRecord] = relationship() text: Mapped[SummaryString] = mapped_column(nullable=False) - media_details: Mapped[MediaDetails] = mapped_column() + media_details: Mapped[List[PostMediaAssociation]] = relationship() created_at: Mapped[TwitterTimestamp] = mapped_column(nullable=False) like_count: Mapped[NonNegativeInt] = mapped_column(nullable=False) repost_count: Mapped[NonNegativeInt] = mapped_column(nullable=False) @@ -236,7 +268,29 @@ def engine(self) -> Engine: return self._engine @classmethod - def _post_record_to_model(cls, post_record: PostRecord) -> PostModel: + def _media_record_to_model(cls, media_record: MediaRecord) -> XMedia: + return XMedia( + media_key=media_record.media_key, + type=media_record.type, + url=media_record.url, + width=media_record.width, + height=media_record.height, + ) + + @classmethod + def _post_record_media_details_to_model(cls, post_record: PostRecord) -> MediaDetails: + if post_record.media_details is None: + return None + if post_record.media_details == []: + return [] + return [cls._media_record_to_model(post_media.media) for post_media in post_record.media_details] + + @classmethod + def _post_record_to_model(cls, post_record: PostRecord, *, with_media: bool) -> PostModel: + # post_record.media_detailsにアクセスしたタイミングでメディア情報を一気に引っ張るクエリが発行される + # media情報がいらない場合はクエリを発行したくないので先にwith_mediaをチェック + media_details = cls._post_record_media_details_to_model(post_record) if with_media else None + return PostModel( post_id=post_record.post_id, x_user_id=post_record.user_id, @@ -248,7 +302,7 @@ def _post_record_to_model(cls, post_record: PostRecord) -> PostModel: following_count=post_record.user.following_count, ), text=post_record.text, - media_details=post_record.media_details, + media_details=media_details, created_at=post_record.created_at, like_count=post_record.like_count, repost_count=post_record.repost_count, @@ -340,6 +394,7 @@ def get_posts( search_url: Union[HttpUrl, None] = None, offset: Union[int, None] = None, limit: int = 100, + with_media: bool = True, ) -> Generator[PostModel, None, None]: with Session(self.engine) as sess: query = sess.query(PostRecord) @@ -365,7 +420,7 @@ def get_posts( query = query.offset(offset) query = query.limit(limit) for post_record in query.all(): - yield self._post_record_to_model(post_record) + yield self._post_record_to_model(post_record, with_media=with_media) def get_number_of_posts( self, From cb73de282de689db034f92fb9c8a2a388e78adff Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 19:37:48 +0900 Subject: [PATCH 03/14] =?UTF-8?q?=E3=83=A1=E3=83=87=E3=82=A3=E3=82=A2?= =?UTF-8?q?=E6=83=85=E5=A0=B1=E3=82=92=E5=8F=96=E5=BE=97=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=8B=E6=8C=87=E5=AE=9A=E3=81=99=E3=82=8BURL=20param?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/birdxplorer_api/routers/data.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index 09c5f2e..820105b 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -1,10 +1,6 @@ from datetime import timezone from typing import List, Union -from dateutil.parser import parse as dateutil_parse -from fastapi import APIRouter, HTTPException, Query, Request -from pydantic import HttpUrl - from birdxplorer_common.models import ( BaseModel, LanguageIdentifier, @@ -20,6 +16,9 @@ UserEnrollment, ) from birdxplorer_common.storage import Storage +from dateutil.parser import parse as dateutil_parse +from fastapi import APIRouter, HTTPException, Query, Request +from pydantic import HttpUrl class TopicListResponse(BaseModel): @@ -104,6 +103,7 @@ def get_posts( limit: int = Query(default=100, gt=0, le=1000), search_text: Union[None, str] = Query(default=None), search_url: Union[None, HttpUrl] = Query(default=None), + media: bool = Query(default=True), ) -> PostListResponse: if created_at_from is not None and isinstance(created_at_from, str): created_at_from = ensure_twitter_timestamp(created_at_from) @@ -119,6 +119,7 @@ def get_posts( search_url=search_url, offset=offset, limit=limit, + with_media=media, ) ) total_count = storage.get_number_of_posts( From 17c5cf344624a9c24a425a746d70231c015f79d1 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 20:04:34 +0900 Subject: [PATCH 04/14] =?UTF-8?q?MediaDetails=E3=81=A7None=E3=82=92?= =?UTF-8?q?=E8=A8=B1=E5=AE=B9=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/models.py | 5 +++-- common/birdxplorer_common/storage.py | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index b1a7f1b..c7d9821 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -25,6 +25,7 @@ model_validator, ) from pydantic import Field as PydanticField +from pydantic.alias_generators import to_camel from pydantic_core import core_schema IntT = TypeVar("IntT", bound="BaseInt") @@ -705,7 +706,7 @@ class XMedia(BaseModel): height: NonNegativeInt -MediaDetails: TypeAlias = list[XMedia] | None +MediaDetails: TypeAlias = List[XMedia] class LinkId(UUID): @@ -776,7 +777,7 @@ class Post(BaseModel): x_user_id: UserId x_user: XUser text: str - media_details: Annotated[MediaDetails, PydanticField(default_factory=lambda: None)] + media_details: Annotated[MediaDetails, PydanticField(default_factory=lambda: [])] created_at: TwitterTimestamp like_count: NonNegativeInt repost_count: NonNegativeInt diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 0645163..5066c29 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -279,8 +279,6 @@ def _media_record_to_model(cls, media_record: MediaRecord) -> XMedia: @classmethod def _post_record_media_details_to_model(cls, post_record: PostRecord) -> MediaDetails: - if post_record.media_details is None: - return None if post_record.media_details == []: return [] return [cls._media_record_to_model(post_media.media) for post_media in post_record.media_details] @@ -289,7 +287,7 @@ def _post_record_media_details_to_model(cls, post_record: PostRecord) -> MediaDe def _post_record_to_model(cls, post_record: PostRecord, *, with_media: bool) -> PostModel: # post_record.media_detailsにアクセスしたタイミングでメディア情報を一気に引っ張るクエリが発行される # media情報がいらない場合はクエリを発行したくないので先にwith_mediaをチェック - media_details = cls._post_record_media_details_to_model(post_record) if with_media else None + media_details = cls._post_record_media_details_to_model(post_record) if with_media else [] return PostModel( post_id=post_record.post_id, From e4442ad2753fea1db76f364e15ce3486b47f532b Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 20:04:49 +0900 Subject: [PATCH 05/14] =?UTF-8?q?=E6=97=A2=E5=AD=98=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=81=AEmedia=5Fdetails=E3=81=AE=E5=9E=8B=E3=82=92?= =?UTF-8?q?=E5=90=88=E3=82=8F=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/tests/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/tests/conftest.py b/common/tests/conftest.py index ab3707b..3e0795a 100644 --- a/common/tests/conftest.py +++ b/common/tests/conftest.py @@ -239,7 +239,7 @@ def post_samples( 新しいプロジェクトがついに公開されました!詳細はこちら👉 https://t.co/xxxxxxxxxxx/ #プロジェクト #新発売 #Tech""", - media_details=None, + media_details=[], created_at=1152921600000, like_count=10, repost_count=20, @@ -255,7 +255,7 @@ def post_samples( このブログ記事、めちゃくちゃ参考になった!🔥 チェックしてみて! https://t.co/yyyyyyyyyyy/ #学び #自己啓発""", - media_details=None, + media_details=[], created_at=1153921700000, like_count=10, repost_count=20, @@ -269,7 +269,7 @@ def post_samples( x_user=x_user_samples[1], text="""\ 次の休暇はここに決めた!🌴🏖️ 見てみて~ https://t.co/xxxxxxxxxxx/ https://t.co/wwwwwwwwwww/ #旅行 #バケーション""", - media_details=None, + media_details=[], created_at=1154921800000, like_count=10, repost_count=20, @@ -282,7 +282,7 @@ def post_samples( x_user_id="1234567890123456782", x_user=x_user_samples[1], text="https://t.co/zzzzzzzzzzz/ https://t.co/wwwwwwwwwww/", - media_details=None, + media_details=[], created_at=1154922900000, like_count=10, repost_count=20, @@ -295,7 +295,7 @@ def post_samples( x_user_id="1234567890123456783", x_user=x_user_samples[2], text="empty", - media_details=None, + media_details=[], created_at=1154923900000, like_count=10, repost_count=20, From f26dc8005a2ceb3752f85e85ee728e70f4b007c7 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 20:10:30 +0900 Subject: [PATCH 06/14] =?UTF-8?q?API=E5=81=B4=E6=97=A2=E5=AD=98=E3=83=86?= =?UTF-8?q?=E3=82=B9=E3=83=88=E3=81=AE=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/tests/conftest.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 141c9f2..2d8ea9c 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -197,7 +197,7 @@ def post_samples( 新しいプロジェクトがついに公開されました!詳細はこちら👉 https://t.co/xxxxxxxxxxx/ #プロジェクト #新発売 #Tech""", - media_details=None, + media_details=[], created_at=1152921600000, like_count=10, repost_count=20, @@ -213,7 +213,7 @@ def post_samples( このブログ記事、めちゃくちゃ参考になった!🔥 チェックしてみて! https://t.co/yyyyyyyyyyy/ #学び #自己啓発""", - media_details=None, + media_details=[], created_at=1153921700000, like_count=10, repost_count=20, @@ -227,7 +227,7 @@ def post_samples( x_user=x_user_samples[1], text="""\ 次の休暇はここに決めた!🌴🏖️ 見てみて~ https://t.co/xxxxxxxxxxx/ https://t.co/wwwwwwwwwww/ #旅行 #バケーション""", - media_details=None, + media_details=[], created_at=1154921800000, like_count=10, repost_count=20, @@ -240,7 +240,7 @@ def post_samples( x_user_id="1234567890123456782", x_user=x_user_samples[1], text="https://t.co/zzzzzzzzzzz/ https://t.co/wwwwwwwwwww/", - media_details=None, + media_details=[], created_at=1154922900000, like_count=10, repost_count=20, @@ -253,7 +253,7 @@ def post_samples( x_user_id="1234567890123456783", x_user=x_user_samples[2], text="empty", - media_details=None, + media_details=[], created_at=1154923900000, like_count=10, repost_count=20, @@ -325,6 +325,7 @@ def _get_posts( search_url: Union[HttpUrl, None] = None, offset: Union[int, None] = None, limit: Union[int, None] = None, + with_media: bool = True, ) -> Generator[Post, None, None]: gen_count = 0 actual_gen_count = 0 @@ -354,6 +355,8 @@ def _get_posts( if offset is not None and gen_count <= offset: continue actual_gen_count += 1 + if not with_media: + post.media_details = [] yield post mock.get_posts.side_effect = _get_posts From 20dff3cf8c7f7f833e0385e6a8d6d7d59e66cb5c Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Wed, 2 Oct 2024 11:14:08 +0900 Subject: [PATCH 07/14] =?UTF-8?q?=E5=90=8D=E7=A7=B0=E3=82=92Media=E3=81=AB?= =?UTF-8?q?=E7=B5=B1=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/models.py | 8 ++++---- common/birdxplorer_common/storage.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index c7d9821..3e54f8e 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -694,19 +694,19 @@ class XUser(BaseModel): # ref: https://developer.x.com/en/docs/x-api/data-dictionary/object-model/media -XMediaType: TypeAlias = Literal["photo", "video", "animated_gif"] +MediaType: TypeAlias = Literal["photo", "video", "animated_gif"] -class XMedia(BaseModel): +class Media(BaseModel): media_key: str - type: XMediaType + type: MediaType url: HttpUrl width: NonNegativeInt height: NonNegativeInt -MediaDetails: TypeAlias = List[XMedia] +MediaDetails: TypeAlias = List[Media] class LinkId(UUID): diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 5066c29..4b07f26 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -11,7 +11,9 @@ BinaryBool, LanguageIdentifier, LinkId, + Media, MediaDetails, + MediaType, NonNegativeInt, NoteId, NotesClassification, @@ -25,8 +27,6 @@ UserEnrollment, UserId, UserName, - XMedia, - XMediaType, ) from .models import Link as LinkModel from .models import Note as NoteModel @@ -120,18 +120,18 @@ class PostMediaAssociation(Base): __tablename__ = "post_media" post_id: Mapped[PostId] = mapped_column(ForeignKey("posts.post_id"), primary_key=True) - media_key: Mapped[str] = mapped_column(ForeignKey("x_medias.media_key"), primary_key=True) + media_key: Mapped[str] = mapped_column(ForeignKey("media.media_key"), primary_key=True) # このテーブルにアクセスした時点でほぼ間違いなく MediaRecord も必要なので一気に引っ張る media: Mapped["MediaRecord"] = relationship(back_populates="post_media_association", lazy="joined") class MediaRecord(Base): - __tablename__ = "x_medias" + __tablename__ = "media" media_key: Mapped[str] = mapped_column(primary_key=True) - type: Mapped[XMediaType] = mapped_column(nullable=False) + type: Mapped[MediaType] = mapped_column(nullable=False) url: Mapped[HttpUrl] = mapped_column(nullable=False) width: Mapped[NonNegativeInt] = mapped_column(nullable=False) height: Mapped[NonNegativeInt] = mapped_column(nullable=False) @@ -268,8 +268,8 @@ def engine(self) -> Engine: return self._engine @classmethod - def _media_record_to_model(cls, media_record: MediaRecord) -> XMedia: - return XMedia( + def _media_record_to_model(cls, media_record: MediaRecord) -> Media: + return Media( media_key=media_record.media_key, type=media_record.type, url=media_record.url, From 3280f98c561771e4c7ae44636109fe7c3a1767b5 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Wed, 2 Oct 2024 14:35:20 +0900 Subject: [PATCH 08/14] =?UTF-8?q?common=E3=81=ABMedia=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=81=AB=E9=96=A2=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/tests/conftest.py | 100 ++++++++++++++++++++++++++++------- common/tests/test_storage.py | 2 + 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/common/tests/conftest.py b/common/tests/conftest.py index 3e0795a..e8430ee 100644 --- a/common/tests/conftest.py +++ b/common/tests/conftest.py @@ -3,19 +3,9 @@ from collections.abc import Generator from typing import List, Type -from dotenv import load_dotenv -from polyfactory import Use -from polyfactory.factories.pydantic_factory import ModelFactory -from polyfactory.pytest_plugin import register_fixture -from pytest import fixture -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from sqlalchemy.sql import text - from birdxplorer_common.models import ( Link, + Media, Note, Post, Topic, @@ -27,13 +17,25 @@ from birdxplorer_common.storage import ( Base, LinkRecord, + MediaRecord, NoteRecord, NoteTopicAssociation, PostLinkAssociation, + PostMediaAssociation, PostRecord, TopicRecord, XUserRecord, ) +from dotenv import load_dotenv +from polyfactory import Use +from polyfactory.factories.pydantic_factory import ModelFactory +from polyfactory.pytest_plugin import register_fixture +from pytest import fixture +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from sqlalchemy.sql import text def gen_random_twitter_timestamp() -> int: @@ -97,6 +99,11 @@ class XUserFactory(ModelFactory[XUser]): __model__ = XUser +@register_fixture(name="media_factory") +class MediaFactory(ModelFactory[Media]): + __model__ = Media + + @register_fixture(name="post_factory") class PostFactory(ModelFactory[Post]): __model__ = Post @@ -225,9 +232,36 @@ def x_user_samples(x_user_factory: XUserFactory) -> Generator[List[XUser], None, yield x_users +@fixture +def media_samples(media_factory: MediaFactory) -> Generator[List[Media], None, None]: + yield [ + media_factory.build( + media_key="1234567890123456781", + url="https://pbs.twimg.com/media/xxxxxxxxxxxxxxx.jpg", + type="photo", + width=100, + height=100, + ), + media_factory.build( + media_key="1234567890123456782", + url="https://pbs.twimg.com/media/yyyyyyyyyyyyyyy.mp4", + type="video", + width=200, + height=200, + ), + media_factory.build( + media_key="1234567890123456783", + url="https://pbs.twimg.com/media/zzzzzzzzzzzzzzz.gif", + type="animated_gif", + width=300, + height=300, + ), + ] + + @fixture def post_samples( - post_factory: PostFactory, x_user_samples: List[XUser], link_samples: List[Link] + post_factory: PostFactory, x_user_samples: List[XUser], media_samples: List[Media], link_samples: List[Link] ) -> Generator[List[Post], None, None]: posts = [ post_factory.build( @@ -255,7 +289,7 @@ def post_samples( このブログ記事、めちゃくちゃ参考になった!🔥 チェックしてみて! https://t.co/yyyyyyyyyyy/ #学び #自己啓発""", - media_details=[], + media_details=[media_samples[0]], created_at=1153921700000, like_count=10, repost_count=20, @@ -268,8 +302,8 @@ def post_samples( x_user_id="1234567890123456782", x_user=x_user_samples[1], text="""\ -次の休暇はここに決めた!🌴🏖️ 見てみて~ https://t.co/xxxxxxxxxxx/ https://t.co/wwwwwwwwwww/ #旅行 #バケーション""", - media_details=[], +次の休暇はここに決めた!🌴🏖️ 見てみて~ https://t.co/xxxxxxxxxxx/ #旅行 #バケーション""", + media_details=[media_samples[1], media_samples[2]], created_at=1154921800000, like_count=10, repost_count=20, @@ -419,8 +453,8 @@ def x_user_records_sample( @fixture def link_records_sample( - link_samples: List[Link], engine_for_test: Engine, + link_samples: List[Link], ) -> Generator[List[LinkRecord], None, None]: res = [LinkRecord(link_id=d.link_id, url=d.url) for d in link_samples] with Session(engine_for_test) as sess: @@ -429,9 +463,31 @@ def link_records_sample( yield res +@fixture +def media_records_sample( + engine_for_test: Engine, + media_samples: List[Media], +) -> Generator[List[MediaRecord], None, None]: + res = [ + MediaRecord( + media_key=d.media_key, + type=d.type, + url=d.url, + width=d.width, + height=d.height, + ) + for d in media_samples + ] + with Session(engine_for_test) as sess: + sess.add_all(res) + sess.commit() + yield res + + @fixture def post_records_sample( x_user_records_sample: List[XUserRecord], + media_records_sample: List[MediaRecord], link_records_sample: List[LinkRecord], post_samples: List[Post], engine_for_test: Engine, @@ -451,9 +507,15 @@ def post_records_sample( ) sess.add(inst) for link in post.links: - assoc = PostLinkAssociation(link_id=link.link_id, post_id=inst.post_id) - sess.add(assoc) - inst.links.append(assoc) + post_link_assoc = PostLinkAssociation(link_id=link.link_id, post_id=inst.post_id) + sess.add(post_link_assoc) + inst.links.append(post_link_assoc) + + for media in post.media_details: + post_media_assoc = PostMediaAssociation(media_key=media.media_key, post_id=inst.post_id) + sess.add(post_media_assoc) + inst.media_details.append(post_media_assoc) + res.append(inst) sess.commit() yield res diff --git a/common/tests/test_storage.py b/common/tests/test_storage.py index 079056a..07d6fb2 100644 --- a/common/tests/test_storage.py +++ b/common/tests/test_storage.py @@ -45,6 +45,8 @@ def test_get_topic_list( [dict(search_url=HttpUrl("https://example.com/sh3")), [2, 3]], [dict(note_ids=[NoteId.from_str("1234567890123456781")]), [0]], [dict(offset=1, limit=1, search_text="https://t.co/xxxxxxxxxxx/"), [2]], + [dict(with_media=True), [0, 1, 2]], + [dict(post_ids=[PostId.from_str("2234567890123456781")], with_media=False), [0]], ], ) def test_get_post( From 98a8dc2d0bc748c34c0f9b1fc193133046bc100b Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Wed, 2 Oct 2024 15:19:45 +0900 Subject: [PATCH 09/14] =?UTF-8?q?API=E5=81=B4=E3=81=AB=E3=83=86=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/tests/conftest.py | 46 ++++++++++++++++++++++++++++++---- api/tests/routers/test_data.py | 34 +++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 2d8ea9c..35572e1 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -17,6 +17,7 @@ LanguageIdentifier, Link, LinkId, + Media, Note, NoteId, ParticipantId, @@ -64,6 +65,11 @@ class XUserFactory(ModelFactory[XUser]): __model__ = XUser +@register_fixture(name="media_factory") +class MediaFactory(ModelFactory[Media]): + __model__ = Media + + @register_fixture(name="post_factory") class PostFactory(ModelFactory[Post]): __model__ = Post @@ -183,9 +189,36 @@ def x_user_samples(x_user_factory: XUserFactory) -> Generator[List[XUser], None, yield x_users +@fixture +def media_samples(media_factory: MediaFactory) -> Generator[List[Media], None, None]: + yield [ + media_factory.build( + media_key="1234567890123456781", + url="https://pbs.twimg.com/media/xxxxxxxxxxxxxxx.jpg", + type="photo", + width=100, + height=100, + ), + media_factory.build( + media_key="1234567890123456782", + url="https://pbs.twimg.com/media/yyyyyyyyyyyyyyy.mp4", + type="video", + width=200, + height=200, + ), + media_factory.build( + media_key="1234567890123456783", + url="https://pbs.twimg.com/media/zzzzzzzzzzzzzzz.gif", + type="animated_gif", + width=300, + height=300, + ), + ] + + @fixture def post_samples( - post_factory: PostFactory, x_user_samples: List[XUser], link_samples: List[Link] + post_factory: PostFactory, x_user_samples: List[XUser], media_samples: List[Media],link_samples: List[Link] ) -> Generator[List[Post], None, None]: posts = [ post_factory.build( @@ -213,7 +246,7 @@ def post_samples( このブログ記事、めちゃくちゃ参考になった!🔥 チェックしてみて! https://t.co/yyyyyyyyyyy/ #学び #自己啓発""", - media_details=[], + media_details=[media_samples[0]], created_at=1153921700000, like_count=10, repost_count=20, @@ -268,6 +301,7 @@ def post_samples( def mock_storage( user_enrollment_samples: List[UserEnrollment], topic_samples: List[Topic], + media_samples: List[Media], post_samples: List[Post], note_samples: List[Note], link_samples: List[Link], @@ -355,9 +389,11 @@ def _get_posts( if offset is not None and gen_count <= offset: continue actual_gen_count += 1 - if not with_media: - post.media_details = [] - yield post + + if with_media is False: + yield post.model_copy(update={"media_details": []}, deep=True) + else: + yield post mock.get_posts.side_effect = _get_posts diff --git a/api/tests/routers/test_data.py b/api/tests/routers/test_data.py index e35f6e4..e971098 100644 --- a/api/tests/routers/test_data.py +++ b/api/tests/routers/test_data.py @@ -122,6 +122,40 @@ def test_posts_get_timestamp_out_of_range(client: TestClient, post_samples: List assert response.status_code == 422 +def test_posts_get_with_media_by_default(client: TestClient, post_samples: List[Post]) -> None: + response = client.get("/api/v1/data/posts/?postId=2234567890123456791") + + assert response.status_code == 200 + res_json_default = response.json() + assert res_json_default == { + "data": [json.loads(post_samples[1].model_dump_json())], + "meta": {"next": None, "prev": None}, + } + + +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") + + assert response.status_code == 200 + res_json_default = response.json() + assert res_json_default == { + "data": [json.loads(post_samples[1].model_dump_json())], + "meta": {"next": None, "prev": None}, + } + + +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") + + assert response.status_code == 200 + res_json_default = response.json() + assert res_json_default == { + "data": [json.loads(expected_post.model_dump_json())], + "meta": {"next": None, "prev": None}, + } + + def test_posts_search_by_text(client: TestClient, post_samples: List[Post]) -> None: response = client.get("/api/v1/data/posts/?searchText=https%3A%2F%2Ft.co%2Fxxxxxxxxxxx%2F") assert response.status_code == 200 From f466efde40f8083cc8ca26071c1d38fdcd925fae Mon Sep 17 00:00:00 2001 From: sushichan044 Date: Tue, 8 Oct 2024 17:35:57 +0900 Subject: [PATCH 10/14] fix: error after rebase --- api/birdxplorer_api/routers/data.py | 7 ++++--- api/tests/conftest.py | 2 +- common/birdxplorer_common/models.py | 11 ++++------- common/birdxplorer_common/storage.py | 25 ++++++++----------------- common/tests/conftest.py | 23 ++++++++++++----------- common/tests/test_storage.py | 2 +- 6 files changed, 30 insertions(+), 40 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index 820105b..e0ef426 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -1,6 +1,10 @@ from datetime import timezone from typing import List, Union +from dateutil.parser import parse as dateutil_parse +from fastapi import APIRouter, HTTPException, Query, Request +from pydantic import HttpUrl + from birdxplorer_common.models import ( BaseModel, LanguageIdentifier, @@ -16,9 +20,6 @@ UserEnrollment, ) from birdxplorer_common.storage import Storage -from dateutil.parser import parse as dateutil_parse -from fastapi import APIRouter, HTTPException, Query, Request -from pydantic import HttpUrl class TopicListResponse(BaseModel): diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 35572e1..affa005 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -218,7 +218,7 @@ def media_samples(media_factory: MediaFactory) -> Generator[List[Media], None, N @fixture def post_samples( - post_factory: PostFactory, x_user_samples: List[XUser], media_samples: List[Media],link_samples: List[Link] + post_factory: PostFactory, x_user_samples: List[XUser], media_samples: List[Media], link_samples: List[Link] ) -> Generator[List[Post], None, None]: posts = [ post_factory.build( diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 3e54f8e..908d52a 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -17,17 +17,14 @@ from uuid import UUID from pydantic import BaseModel as PydanticBaseModel -from pydantic import ( - ConfigDict, - GetCoreSchemaHandler, - HttpUrl, - TypeAdapter, - model_validator, -) +from pydantic import ConfigDict from pydantic import Field as PydanticField +from pydantic import GetCoreSchemaHandler, HttpUrl, TypeAdapter, model_validator from pydantic.alias_generators import to_camel +from pydantic.main import IncEx from pydantic_core import core_schema +StrT = TypeVar("StrT", bound="BaseString") IntT = TypeVar("IntT", bound="BaseInt") FloatT = TypeVar("FloatT", bound="BaseFloat") diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 4b07f26..6eee4af 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -7,20 +7,15 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from sqlalchemy.types import CHAR, DECIMAL, JSON, Integer, String, Uuid +from .models import BinaryBool, LanguageIdentifier +from .models import Link as LinkModel +from .models import LinkId, Media, MediaDetails, MediaType, NonNegativeInt +from .models import Note as NoteModel +from .models import NoteId, NotesClassification, NotesHarmful, ParticipantId +from .models import Post as PostModel +from .models import PostId, SummaryString +from .models import Topic as TopicModel from .models import ( - BinaryBool, - LanguageIdentifier, - LinkId, - Media, - MediaDetails, - MediaType, - NonNegativeInt, - NoteId, - NotesClassification, - NotesHarmful, - ParticipantId, - PostId, - SummaryString, TopicId, TopicLabel, TwitterTimestamp, @@ -28,10 +23,6 @@ UserId, UserName, ) -from .models import Link as LinkModel -from .models import Note as NoteModel -from .models import Post as PostModel -from .models import Topic as TopicModel from .models import XUser as XUserModel from .settings import GlobalSettings diff --git a/common/tests/conftest.py b/common/tests/conftest.py index e8430ee..78865d7 100644 --- a/common/tests/conftest.py +++ b/common/tests/conftest.py @@ -3,6 +3,17 @@ from collections.abc import Generator from typing import List, Type +from dotenv import load_dotenv +from polyfactory import Use +from polyfactory.factories.pydantic_factory import ModelFactory +from polyfactory.pytest_plugin import register_fixture +from pytest import fixture +from sqlalchemy import create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy.orm import Session +from sqlalchemy.sql import text + from birdxplorer_common.models import ( Link, Media, @@ -26,16 +37,6 @@ TopicRecord, XUserRecord, ) -from dotenv import load_dotenv -from polyfactory import Use -from polyfactory.factories.pydantic_factory import ModelFactory -from polyfactory.pytest_plugin import register_fixture -from pytest import fixture -from sqlalchemy import create_engine -from sqlalchemy.engine import Engine -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session -from sqlalchemy.sql import text def gen_random_twitter_timestamp() -> int: @@ -499,13 +500,13 @@ def post_records_sample( post_id=post.post_id, user_id=post.x_user_id, text=post.text, - media_details=post.media_details, created_at=post.created_at, like_count=post.like_count, repost_count=post.repost_count, impression_count=post.impression_count, ) sess.add(inst) + for link in post.links: post_link_assoc = PostLinkAssociation(link_id=link.link_id, post_id=inst.post_id) sess.add(post_link_assoc) diff --git a/common/tests/test_storage.py b/common/tests/test_storage.py index 07d6fb2..ce854b1 100644 --- a/common/tests/test_storage.py +++ b/common/tests/test_storage.py @@ -45,7 +45,7 @@ def test_get_topic_list( [dict(search_url=HttpUrl("https://example.com/sh3")), [2, 3]], [dict(note_ids=[NoteId.from_str("1234567890123456781")]), [0]], [dict(offset=1, limit=1, search_text="https://t.co/xxxxxxxxxxx/"), [2]], - [dict(with_media=True), [0, 1, 2]], + [dict(with_media=True), [0, 1, 2, 3, 4]], [dict(post_ids=[PostId.from_str("2234567890123456781")], with_media=False), [0]], ], ) From 0fe5aea38748507f0bd0a6ed2ede0872b7e68983 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 19:33:09 +0900 Subject: [PATCH 11/14] =?UTF-8?q?MediaDetails=E3=82=92=E5=AE=9A=E7=BE=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 908d52a..95df64c 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -15,11 +15,14 @@ Union, ) from uuid import UUID +from typing import Annotated, Any, Dict, List, Literal, Optional, Type, TypeAlias, TypeVar, Union from pydantic import BaseModel as PydanticBaseModel from pydantic import ConfigDict from pydantic import Field as PydanticField from pydantic import GetCoreSchemaHandler, HttpUrl, TypeAdapter, model_validator +from pydantic import BaseModel as PydanticBaseModel, Field as PydanticField +from pydantic import ConfigDict, GetCoreSchemaHandler, HttpUrl, TypeAdapter from pydantic.alias_generators import to_camel from pydantic.main import IncEx from pydantic_core import core_schema From d90f0a9887e9cf73e06868141987784a26d0721f Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 19:33:26 +0900 Subject: [PATCH 12/14] =?UTF-8?q?MediaDetails=E3=82=92DB=E5=81=B4=E3=81=A7?= =?UTF-8?q?=E8=A1=A8=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/storage.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 6eee4af..4b07f26 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -7,15 +7,20 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from sqlalchemy.types import CHAR, DECIMAL, JSON, Integer, String, Uuid -from .models import BinaryBool, LanguageIdentifier -from .models import Link as LinkModel -from .models import LinkId, Media, MediaDetails, MediaType, NonNegativeInt -from .models import Note as NoteModel -from .models import NoteId, NotesClassification, NotesHarmful, ParticipantId -from .models import Post as PostModel -from .models import PostId, SummaryString -from .models import Topic as TopicModel from .models import ( + BinaryBool, + LanguageIdentifier, + LinkId, + Media, + MediaDetails, + MediaType, + NonNegativeInt, + NoteId, + NotesClassification, + NotesHarmful, + ParticipantId, + PostId, + SummaryString, TopicId, TopicLabel, TwitterTimestamp, @@ -23,6 +28,10 @@ UserId, UserName, ) +from .models import Link as LinkModel +from .models import Note as NoteModel +from .models import Post as PostModel +from .models import Topic as TopicModel from .models import XUser as XUserModel from .settings import GlobalSettings From 0c58393da4bc542ac60bee8680c3283802dbacf6 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Mon, 30 Sep 2024 20:04:34 +0900 Subject: [PATCH 13/14] =?UTF-8?q?MediaDetails=E3=81=A7None=E3=82=92?= =?UTF-8?q?=E8=A8=B1=E5=AE=B9=E3=81=97=E3=81=AA=E3=81=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/birdxplorer_common/models.py | 3 --- common/birdxplorer_common/storage.py | 25 ++++++++----------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 95df64c..908d52a 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -15,14 +15,11 @@ Union, ) from uuid import UUID -from typing import Annotated, Any, Dict, List, Literal, Optional, Type, TypeAlias, TypeVar, Union from pydantic import BaseModel as PydanticBaseModel from pydantic import ConfigDict from pydantic import Field as PydanticField from pydantic import GetCoreSchemaHandler, HttpUrl, TypeAdapter, model_validator -from pydantic import BaseModel as PydanticBaseModel, Field as PydanticField -from pydantic import ConfigDict, GetCoreSchemaHandler, HttpUrl, TypeAdapter from pydantic.alias_generators import to_camel from pydantic.main import IncEx from pydantic_core import core_schema diff --git a/common/birdxplorer_common/storage.py b/common/birdxplorer_common/storage.py index 4b07f26..6eee4af 100644 --- a/common/birdxplorer_common/storage.py +++ b/common/birdxplorer_common/storage.py @@ -7,20 +7,15 @@ from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship from sqlalchemy.types import CHAR, DECIMAL, JSON, Integer, String, Uuid +from .models import BinaryBool, LanguageIdentifier +from .models import Link as LinkModel +from .models import LinkId, Media, MediaDetails, MediaType, NonNegativeInt +from .models import Note as NoteModel +from .models import NoteId, NotesClassification, NotesHarmful, ParticipantId +from .models import Post as PostModel +from .models import PostId, SummaryString +from .models import Topic as TopicModel from .models import ( - BinaryBool, - LanguageIdentifier, - LinkId, - Media, - MediaDetails, - MediaType, - NonNegativeInt, - NoteId, - NotesClassification, - NotesHarmful, - ParticipantId, - PostId, - SummaryString, TopicId, TopicLabel, TwitterTimestamp, @@ -28,10 +23,6 @@ UserId, UserName, ) -from .models import Link as LinkModel -from .models import Note as NoteModel -from .models import Post as PostModel -from .models import Topic as TopicModel from .models import XUser as XUserModel from .settings import GlobalSettings From 494dc98b65acc5fb80b76e91dd53cc43c6463e64 Mon Sep 17 00:00:00 2001 From: sushi-chaaaan Date: Wed, 2 Oct 2024 16:47:30 +0900 Subject: [PATCH 14/14] =?UTF-8?q?Post.link=E3=82=92computed=20field?= =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/birdxplorer_api/routers/data.py | 3 --- common/birdxplorer_common/models.py | 26 ++++++++++++++++++++++++-- common/tests/conftest.py | 3 --- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/api/birdxplorer_api/routers/data.py b/api/birdxplorer_api/routers/data.py index e0ef426..e51e13d 100644 --- a/api/birdxplorer_api/routers/data.py +++ b/api/birdxplorer_api/routers/data.py @@ -132,9 +132,6 @@ def get_posts( search_url=search_url, ) - for post in posts: - post.link = HttpUrl(f"https://x.com/{post.x_user.name}/status/{post.post_id}") - base_url = str(request.url).split("?")[0] next_offset = offset + limit prev_offset = max(offset - limit, 0) diff --git a/common/birdxplorer_common/models.py b/common/birdxplorer_common/models.py index 908d52a..25b1e4b 100644 --- a/common/birdxplorer_common/models.py +++ b/common/birdxplorer_common/models.py @@ -19,7 +19,7 @@ from pydantic import BaseModel as PydanticBaseModel from pydantic import ConfigDict from pydantic import Field as PydanticField -from pydantic import GetCoreSchemaHandler, HttpUrl, TypeAdapter, model_validator +from pydantic import GetCoreSchemaHandler, HttpUrl, TypeAdapter, model_validator, computed_field from pydantic.alias_generators import to_camel from pydantic.main import IncEx from pydantic_core import core_schema @@ -770,7 +770,6 @@ def validate_link_id(cls, values: Dict[str, Any]) -> Dict[str, Any]: class Post(BaseModel): post_id: PostId - link: Optional[HttpUrl] = None x_user_id: UserId x_user: XUser text: str @@ -781,6 +780,29 @@ class Post(BaseModel): impression_count: NonNegativeInt links: List[Link] = [] + @property + @computed_field + def link(self) -> HttpUrl: + """ + PostのX上でのURLを返す。 + + Examples + -------- + >>> post = Post(post_id="1234567890123456789", + x_user_id="1234567890123456789", + x_user=XUser(user_id="1234567890123456789", + name="test", + profile_image="https://x.com/test"), + text="test", + created_at=1288834974657, + like_count=1, + repost_count=1, + impression_count=1) + >>> post.link + HttpUrl('https://x.com/test/status/1234567890123456789') + """ + return HttpUrl(f"https://x.com/{self.x_user.name}/status/{self.post_id}") + class PaginationMeta(BaseModel): next: Optional[HttpUrl] = None diff --git a/common/tests/conftest.py b/common/tests/conftest.py index 78865d7..c972acd 100644 --- a/common/tests/conftest.py +++ b/common/tests/conftest.py @@ -267,7 +267,6 @@ def post_samples( posts = [ post_factory.build( post_id="2234567890123456781", - link=None, x_user_id="1234567890123456781", x_user=x_user_samples[0], text="""\ @@ -283,7 +282,6 @@ def post_samples( ), post_factory.build( post_id="2234567890123456791", - link=None, x_user_id="1234567890123456781", x_user=x_user_samples[0], text="""\ @@ -299,7 +297,6 @@ def post_samples( ), post_factory.build( post_id="2234567890123456801", - link=None, x_user_id="1234567890123456782", x_user=x_user_samples[1], text="""\