Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions instapi/client_api/client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from .comments import CommentsEndpoint
from .direct import DirectEndpoint


class Client(
DirectEndpoint,
CommentsEndpoint,
):
pass

Expand Down
29 changes: 29 additions & 0 deletions instapi/client_api/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any, Iterator, Union

from ..types import StrDict
from .base import BaseClient


class CommentsEndpoint(BaseClient):
def media_comments_gen(self, media_id: Union[int, str], **kwargs: Any) -> Iterator[StrDict]:
kwargs.setdefault("can_support_threading", "false")

results = self.media_comments(media_id, **kwargs)
yield from results["comments"]

while any(
(
results.get("has_more_comments") and results.get("next_max_id"),
results.get("has_more_headload_comments") and results.get("next_min_id"),
)
):
if results.get("has_more_comments"):
kwargs["max_id"] = results.get("next_max_id")
else:
kwargs["min_id"] = results.get("next_min_id")

results = self.media_comments(media_id, **kwargs)
yield from results["comments"]


__all__ = ["CommentsEndpoint"]
32 changes: 30 additions & 2 deletions instapi/models/comment.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Iterable, List, Optional

from ..client import client
from ..utils import to_list
from .media import Media

if TYPE_CHECKING:
Expand All @@ -15,7 +18,7 @@ class Comment(Media):
"""

text: str
user: "User"
user: User

def like(self) -> None:
"""
Expand All @@ -34,6 +37,31 @@ def unlike(self) -> None:
client.comment_unlike(self.pk)


class CommentsBoundMixin:
pk: int

def iter_comments(self) -> Iterable[Comment]:
"""
Create generator for iteration over comments, which was attached to the media

:return: generator with comments
"""
for c in client.media_comments_gen(self.pk):
from instapi.models import User

yield Comment.create({**c, "user": User.create(c["user"])})

def comments(self, limit: Optional[int] = None) -> List[Comment]:
"""
Generate list of comments, which was attached to the media

:param limit: number of comments, which will be added to the list
:return: list with comments
"""
return to_list(self.iter_comments(), limit=limit)


__all__ = [
"Comment",
"CommentsBoundMixin",
]
25 changes: 2 additions & 23 deletions instapi/models/feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from ..client import client
from ..types import StrDict
from ..utils import process_many, to_list
from .comment import Comment
from .comment import CommentsBoundMixin
from .resource import ResourceContainer
from .user import User


@dataclass(frozen=True)
class Feed(ResourceContainer):
class Feed(CommentsBoundMixin, ResourceContainer):
"""
This class represent Instagram's feed. It gives opportunity to:
- Get posts from feed
Expand Down Expand Up @@ -120,27 +120,6 @@ def unlike(self) -> None:
"""
client.delete_like(self.pk)

def iter_comments(self) -> Iterable["Comment"]:
"""
Create generator for iteration over comments, which was attached to the post

:return: generator with comments
"""
for result in process_many(client.media_comments, self.pk):
for c in result["comments"]:
c["user"] = User.create(c["user"])

yield from map(Comment.create, result["comments"])

def comments(self, limit: Optional[int] = None) -> List["Comment"]:
"""
Generate list of comments, which was attached to the post

:param limit: number of comments, which will be added to the list
:return: list with comments
"""
return to_list(self.iter_comments(), limit=limit)


__all__ = [
"Feed",
Expand Down
2 changes: 1 addition & 1 deletion instapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def to_list(iterable: Iterable[T], limit: Optional[int] = None) -> List[T]:
return [*limited(iterable, limit=limit)]


def flat(source: List[Iterable[T]]) -> List[T]:
def flat(source: Iterable[Iterable[T]]) -> List[T]:
"""
Unpack list of iterable into single list

Expand Down
66 changes: 66 additions & 0 deletions tests/unit_tests/client_api/test_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from pytest import fixture, mark

from instapi.client_api.comments import CommentsEndpoint
from instapi.utils import flat

from ..conftest import random_int, random_string


@fixture
def comments_endpoint(mocker):
mocker.patch("instagram_private_api.client.Client.login", return_value=None)
return CommentsEndpoint(random_string(), random_string())


def test_comments_gen_no_more_comments(comments_endpoint, mocker):
mock = mocker.patch.object(
comments_endpoint, "media_comments", autospec=True, return_value={"comments": []}
)

media_id = random_int()

assert not [*comments_endpoint.media_comments_gen(media_id)]

mock.assert_called_once_with(media_id, can_support_threading="false")


@mark.parametrize(
"indicator,key,next_key",
[
("has_more_comments", "max_id", "next_max_id"),
("has_more_headload_comments", "min_id", "next_min_id"),
],
ids=["max_id", "min_id"],
)
def test_comments_gen_next_comments(comments_endpoint, mocker, indicator, key, next_key):
side_effects = [
{
"comments": [mocker.Mock() for _ in range(10)],
indicator: True,
next_key: mocker.Mock(),
}
for _ in range(10)
]
side_effects[-1][indicator] = False

mock = mocker.patch.object(
comments_endpoint, "media_comments", autospec=True, side_effect=side_effects
)

comments = [*flat(r["comments"] for r in side_effects)]

media_id = random_int()

assert [*comments_endpoint.media_comments_gen(media_id)] == comments

calls = [mocker.call(media_id, can_support_threading="false")]
calls.extend(
mocker.call(
media_id,
can_support_threading="false",
**{key: r[next_key]},
)
for r in side_effects[:-1]
)

mock.assert_has_calls(calls)
4 changes: 2 additions & 2 deletions tests/unit_tests/models/test_feed.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ def mock_likers(self, mocker, users):
@fixture
def mock_comments(self, mocker, comments):
return mocker.patch(
"instapi.client.client.media_comments",
return_value={"comments": as_dicts(comments)},
"instapi.client.client.media_comments_gen",
return_value=as_dicts(comments),
)

@fixture
Expand Down