Skip to content

Commit 91702f0

Browse files
committed
fix some comments
1 parent 81056ba commit 91702f0

File tree

3 files changed

+145
-107
lines changed

3 files changed

+145
-107
lines changed

api/birdxplorer_api/routers/data.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
UserId,
3232
)
3333
from birdxplorer_common.storage import Storage
34+
from urllib.parse import urlencode
3435

3536
PostsPaginationMetaWithExamples: TypeAlias = Annotated[
3637
PaginationMeta,
@@ -275,7 +276,11 @@ def str_to_twitter_timestamp(s: str) -> TwitterTimestamp:
275276

276277

277278
def ensure_twitter_timestamp(t: Union[str, TwitterTimestamp]) -> TwitterTimestamp:
278-
return str_to_twitter_timestamp(t) if isinstance(t, str) else t
279+
try:
280+
timestamp = str_to_twitter_timestamp(t) if isinstance(t, str) else t
281+
return timestamp
282+
except:
283+
raise ValueError(f"Timestamp out of range")
279284

280285

281286
def gen_router(storage: Storage) -> APIRouter:
@@ -314,10 +319,13 @@ def get_notes(
314319
language: Union[LanguageIdentifier, None] = Query(default=None, **V1DataNotesDocs.params["language"]),
315320
search_text: Union[None, str] = Query(default=None, **V1DataNotesDocs.params["search_text"]),
316321
) -> NoteListResponse:
317-
if created_at_from is not None and isinstance(created_at_from, str):
318-
created_at_from = ensure_twitter_timestamp(created_at_from)
319-
if created_at_to is not None and isinstance(created_at_to, str):
320-
created_at_to = ensure_twitter_timestamp(created_at_to)
322+
try:
323+
if created_at_from is not None and isinstance(created_at_from, str):
324+
created_at_from = ensure_twitter_timestamp(created_at_from)
325+
if created_at_to is not None and isinstance(created_at_to, str):
326+
created_at_to = ensure_twitter_timestamp(created_at_to)
327+
except ValueError as e:
328+
raise HTTPException(status_code=422, detail=str(e))
321329

322330
notes = list(
323331
storage.get_notes(
@@ -374,10 +382,14 @@ def get_posts(
374382
search_url: Union[None, HttpUrl] = Query(default=None, **V1DataPostsDocs.params["search_url"]),
375383
media: bool = Query(default=True, **V1DataPostsDocs.params["media"]),
376384
) -> PostListResponse:
377-
if created_at_from is not None and isinstance(created_at_from, str):
378-
created_at_from = ensure_twitter_timestamp(created_at_from)
379-
if created_at_to is not None and isinstance(created_at_to, str):
380-
created_at_to = ensure_twitter_timestamp(created_at_to)
385+
try:
386+
if created_at_from is not None and isinstance(created_at_from, str):
387+
created_at_from = ensure_twitter_timestamp(created_at_from)
388+
if created_at_to is not None and isinstance(created_at_to, str):
389+
created_at_to = ensure_twitter_timestamp(created_at_to)
390+
except ValueError as e:
391+
raise HTTPException(status_code=422, detail=str(e))
392+
381393
posts = list(
382394
storage.get_posts(
383395
post_ids=post_ids,
@@ -450,10 +462,13 @@ def search(
450462
limit: int = Query(default=100, gt=0, le=1000, **V1DataSearchDocs.params["limit"]),
451463
) -> SearchResponse:
452464
# Convert timestamp strings to TwitterTimestamp objects
453-
if note_created_at_from is not None and isinstance(note_created_at_from, str):
454-
note_created_at_from = ensure_twitter_timestamp(note_created_at_from)
455-
if note_created_at_to is not None and isinstance(note_created_at_to, str):
456-
note_created_at_to = ensure_twitter_timestamp(note_created_at_to)
465+
try:
466+
if note_created_at_from is not None and isinstance(note_created_at_from, str):
467+
note_created_at_from = ensure_twitter_timestamp(note_created_at_from)
468+
if note_created_at_to is not None and isinstance(note_created_at_to, str):
469+
note_created_at_to = ensure_twitter_timestamp(note_created_at_to)
470+
except ValueError as e:
471+
raise HTTPException(status_code=422, detail=str(e))
457472

458473
# Get search results using the optimized storage method
459474
results = []
@@ -512,14 +527,21 @@ def search(
512527

513528
# Generate pagination URLs
514529
base_url = str(request.url).split("?")[0]
530+
query_params = dict(request.query_params)
515531
next_offset = offset + limit
516532
prev_offset = max(offset - limit, 0)
533+
517534
next_url = None
518535
if next_offset < total_count:
519-
next_url = f"{base_url}?offset={next_offset}&limit={limit}"
536+
query_params["offset"] = next_offset
537+
query_params["limit"] = limit
538+
next_url = f"{base_url}?{urlencode(query_params)}"
539+
520540
prev_url = None
521541
if offset > 0:
522-
prev_url = f"{base_url}?offset={prev_offset}&limit={limit}"
542+
query_params["offset"] = prev_offset
543+
query_params["limit"] = limit
544+
prev_url = f"{base_url}?{urlencode(query_params)}"
523545

524546
return SearchResponse(data=results, meta=PaginationMeta(next=next_url, prev=prev_url))
525547

api/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ dev=[
6161
"httpx",
6262
]
6363
prod=[
64-
"birdxplorer_common @ git+https://github.com/codeforjapan/BirdXplorer.git@feature/138#subdirectory=common",
64+
"birdxplorer_common @ git+https://github.com/codeforjapan/BirdXplorer.git@main#subdirectory=common",
6565
"psycopg2",
6666
"gunicorn",
6767
]

common/birdxplorer_common/storage.py

Lines changed: 107 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from typing import Generator, List, Tuple, Union
1+
from typing import Any, Generator, List, Tuple, Union
22

33
from psycopg2.extensions import AsIs, register_adapter
44
from pydantic import AnyUrl, HttpUrl
55
from sqlalchemy import ForeignKey, create_engine, func, select
66
from sqlalchemy.engine import Engine
77
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column, relationship
8+
from sqlalchemy.orm.query import RowReturningQuery
89
from sqlalchemy.types import CHAR, DECIMAL, JSON, Integer, String, Uuid
910

1011
from .models import BinaryBool, LanguageIdentifier
@@ -507,6 +508,72 @@ def get_number_of_posts(
507508
)
508509
return query.count()
509510

511+
def _apply_filters(
512+
self,
513+
query: RowReturningQuery[Tuple[Any, ...]],
514+
note_includes_text: Union[str, None] = None,
515+
note_excludes_text: Union[str, None] = None,
516+
post_includes_text: Union[str, None] = None,
517+
post_excludes_text: Union[str, None] = None,
518+
language: Union[LanguageIdentifier, None] = None,
519+
topic_ids: Union[List[TopicId], None] = None,
520+
note_status: Union[List[str], None] = None,
521+
note_created_at_from: Union[TwitterTimestamp, None] = None,
522+
note_created_at_to: Union[TwitterTimestamp, None] = None,
523+
x_user_names: Union[List[str], None] = None,
524+
x_user_followers_count_from: Union[int, None] = None,
525+
x_user_follow_count_from: Union[int, None] = None,
526+
post_like_count_from: Union[int, None] = None,
527+
post_repost_count_from: Union[int, None] = None,
528+
post_impression_count_from: Union[int, None] = None,
529+
post_includes_media: Union[bool, None] = None,
530+
) -> RowReturningQuery[Tuple[Any, ...]]:
531+
# Apply note filters
532+
if note_includes_text:
533+
query = query.filter(NoteRecord.summary.like(f"%{note_includes_text}%"))
534+
if note_excludes_text:
535+
query = query.filter(~NoteRecord.summary.like(f"%{note_excludes_text}%"))
536+
if language:
537+
query = query.filter(NoteRecord.language == language)
538+
if topic_ids:
539+
subq = (
540+
select(NoteTopicAssociation.note_id)
541+
.filter(NoteTopicAssociation.topic_id.in_(topic_ids))
542+
.group_by(NoteTopicAssociation.note_id)
543+
.subquery()
544+
)
545+
query = query.join(subq, NoteRecord.note_id == subq.c.note_id)
546+
if note_status:
547+
query = query.filter(NoteRecord.current_status.in_(note_status))
548+
if note_created_at_from:
549+
query = query.filter(NoteRecord.created_at >= note_created_at_from)
550+
if note_created_at_to:
551+
query = query.filter(NoteRecord.created_at <= note_created_at_to)
552+
553+
# Apply post filters
554+
if post_includes_text:
555+
query = query.filter(PostRecord.text.like(f"%{post_includes_text}%"))
556+
if post_excludes_text:
557+
query = query.filter(~PostRecord.text.like(f"%{post_excludes_text}%"))
558+
if x_user_names:
559+
query = query.filter(XUserRecord.name.in_(x_user_names))
560+
if x_user_followers_count_from:
561+
query = query.filter(XUserRecord.followers_count >= x_user_followers_count_from)
562+
if x_user_follow_count_from:
563+
query = query.filter(XUserRecord.following_count >= x_user_follow_count_from)
564+
if post_like_count_from:
565+
query = query.filter(PostRecord.like_count >= post_like_count_from)
566+
if post_repost_count_from:
567+
query = query.filter(PostRecord.repost_count >= post_repost_count_from)
568+
if post_impression_count_from:
569+
query = query.filter(PostRecord.impression_count >= post_impression_count_from)
570+
if post_includes_media:
571+
query = query.filter(PostRecord.media_details.any())
572+
elif post_includes_media is False:
573+
query = query.filter(~PostRecord.media_details.any())
574+
575+
return query
576+
510577
def search_notes_with_posts(
511578
self,
512579
note_includes_text: Union[str, None] = None,
@@ -529,61 +596,34 @@ def search_notes_with_posts(
529596
limit: int = 100,
530597
) -> Generator[Tuple[NoteModel, PostModel | None], None, None]:
531598
with Session(self.engine) as sess:
532-
# Base query joining notes, posts and users
533599
query = (
534600
sess.query(NoteRecord, PostRecord)
535601
.outerjoin(PostRecord, NoteRecord.post_id == PostRecord.post_id)
536602
.outerjoin(XUserRecord, PostRecord.user_id == XUserRecord.user_id)
537603
)
538604

539-
# Apply note filters
540-
if note_includes_text:
541-
query = query.filter(NoteRecord.summary.like(f"%{note_includes_text}%"))
542-
if note_excludes_text:
543-
query = query.filter(~NoteRecord.summary.like(f"%{note_excludes_text}%"))
544-
if language:
545-
query = query.filter(NoteRecord.language == language)
546-
if topic_ids:
547-
subq = (
548-
select(NoteTopicAssociation.note_id)
549-
.filter(NoteTopicAssociation.topic_id.in_(topic_ids))
550-
.group_by(NoteTopicAssociation.note_id)
551-
.subquery()
552-
)
553-
query = query.join(subq, NoteRecord.note_id == subq.c.note_id)
554-
if note_status:
555-
query = query.filter(NoteRecord.current_status.in_(note_status))
556-
if note_created_at_from:
557-
query = query.filter(NoteRecord.created_at >= note_created_at_from)
558-
if note_created_at_to:
559-
query = query.filter(NoteRecord.created_at <= note_created_at_to)
560-
561-
# Apply post filters
562-
if post_includes_text:
563-
query = query.filter(PostRecord.text.like(f"%{post_includes_text}%"))
564-
if post_excludes_text:
565-
query = query.filter(~PostRecord.text.like(f"%{post_excludes_text}%"))
566-
if x_user_names:
567-
query = query.filter(XUserRecord.name.in_(x_user_names))
568-
if x_user_followers_count_from:
569-
query = query.filter(XUserRecord.followers_count >= x_user_followers_count_from)
570-
if x_user_follow_count_from:
571-
query = query.filter(XUserRecord.following_count >= x_user_follow_count_from)
572-
if post_like_count_from:
573-
query = query.filter(PostRecord.like_count >= post_like_count_from)
574-
if post_repost_count_from:
575-
query = query.filter(PostRecord.repost_count >= post_repost_count_from)
576-
if post_impression_count_from:
577-
query = query.filter(PostRecord.impression_count >= post_impression_count_from)
578-
if post_includes_media:
579-
query = query.filter(PostRecord.media_details.any())
580-
if post_includes_media is False:
581-
query = query.filter(~PostRecord.media_details.any())
582-
583-
# Pagination
605+
query = self._apply_filters(
606+
query,
607+
note_includes_text,
608+
note_excludes_text,
609+
post_includes_text,
610+
post_excludes_text,
611+
language,
612+
topic_ids,
613+
note_status,
614+
note_created_at_from,
615+
note_created_at_to,
616+
x_user_names,
617+
x_user_followers_count_from,
618+
x_user_follow_count_from,
619+
post_like_count_from,
620+
post_repost_count_from,
621+
post_impression_count_from,
622+
post_includes_media,
623+
)
624+
584625
query = query.offset(offset).limit(limit)
585626

586-
# Execute query and yield results
587627
for note_record, post_record in query.all():
588628
note = NoteModel(
589629
note_id=note_record.note_id,
@@ -627,49 +667,25 @@ def count_search_results(
627667
.outerjoin(XUserRecord, PostRecord.user_id == XUserRecord.user_id)
628668
)
629669

630-
# Apply note filters
631-
if note_includes_text:
632-
query = query.filter(NoteRecord.summary.like(f"%{note_includes_text}%"))
633-
if note_excludes_text:
634-
query = query.filter(~NoteRecord.summary.like(f"%{note_excludes_text}%"))
635-
if language:
636-
query = query.filter(NoteRecord.language == language)
637-
if topic_ids:
638-
subq = (
639-
select(NoteTopicAssociation.note_id)
640-
.filter(NoteTopicAssociation.topic_id.in_(topic_ids))
641-
.group_by(NoteTopicAssociation.note_id)
642-
.subquery()
643-
)
644-
query = query.join(subq, NoteRecord.note_id == subq.c.note_id)
645-
if note_status:
646-
query = query.filter(NoteRecord.current_status.in_(note_status))
647-
if note_created_at_from:
648-
query = query.filter(NoteRecord.created_at >= note_created_at_from)
649-
if note_created_at_to:
650-
query = query.filter(NoteRecord.created_at <= note_created_at_to)
651-
652-
# Apply post filters
653-
if post_includes_text:
654-
query = query.filter(PostRecord.text.like(f"%{post_includes_text}%"))
655-
if post_excludes_text:
656-
query = query.filter(~PostRecord.text.like(f"%{post_excludes_text}%"))
657-
if x_user_names:
658-
query = query.filter(XUserRecord.name.in_(x_user_names))
659-
if x_user_followers_count_from:
660-
query = query.filter(XUserRecord.followers_count >= x_user_followers_count_from)
661-
if x_user_follow_count_from:
662-
query = query.filter(XUserRecord.following_count >= x_user_follow_count_from)
663-
if post_like_count_from:
664-
query = query.filter(PostRecord.like_count >= post_like_count_from)
665-
if post_repost_count_from:
666-
query = query.filter(PostRecord.repost_count >= post_repost_count_from)
667-
if post_impression_count_from:
668-
query = query.filter(PostRecord.impression_count >= post_impression_count_from)
669-
if post_includes_media:
670-
query = query.filter(PostRecord.media_details.any())
671-
elif post_includes_media is False:
672-
query = query.filter(~PostRecord.media_details.any())
670+
query = self._apply_filters(
671+
query,
672+
note_includes_text,
673+
note_excludes_text,
674+
post_includes_text,
675+
post_excludes_text,
676+
language,
677+
topic_ids,
678+
note_status,
679+
note_created_at_from,
680+
note_created_at_to,
681+
x_user_names,
682+
x_user_followers_count_from,
683+
x_user_follow_count_from,
684+
post_like_count_from,
685+
post_repost_count_from,
686+
post_impression_count_from,
687+
post_includes_media,
688+
)
673689

674690
return query.scalar() or 0
675691

0 commit comments

Comments
 (0)