2026-02-01 09:31:38 +01:00

587 lines
18 KiB
Python

"""Provide Collections functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from ...const import API_PATH
from ...exceptions import ClientException
from ...util import _deprecate_args
from ...util.cache import cachedproperty
from ..base import PRAWBase
from .base import RedditBase
from .submission import Submission
from .subreddit import Subreddit
if TYPE_CHECKING: # pragma: no cover
from collections.abc import Iterator
import praw.models
class CollectionModeration(PRAWBase):
"""Class to support moderation actions on a :class:`.Collection`.
Obtain an instance via:
.. code-block:: python
reddit.subreddit("test").collections("some_uuid").mod
"""
def __init__(self, reddit: praw.Reddit, collection_id: str):
"""Initialize a :class:`.CollectionModeration` instance.
:param collection_id: The ID of a :class:`.Collection`.
"""
super().__init__(reddit, _data=None)
self.collection_id = collection_id
def _post_fullname(self, post: str | praw.models.Submission) -> str:
"""Get a post's fullname.
:param post: A fullname, a :class:`.Submission`, a permalink, or an ID.
:returns: The fullname of the post.
"""
if isinstance(post, Submission):
return post.fullname
if not isinstance(post, str):
msg = f"Cannot get fullname from object of type {type(post)}."
raise TypeError(msg)
if post.startswith(f"{self._reddit.config.kinds['submission']}_"):
return post
try:
return self._reddit.submission(url=post).fullname
except ClientException:
return self._reddit.submission(post).fullname
def add_post(self, submission: praw.models.Submission):
"""Add a post to the collection.
:param submission: The post to add, a :class:`.Submission`, its permalink as a
``str``, its fullname as a ``str``, or its ID as a ``str``.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.add_post("bgibu9")
.. seealso::
:meth:`.remove_post`
"""
link_fullname = self._post_fullname(submission)
self._reddit.post(
API_PATH["collection_add_post"],
data={"collection_id": self.collection_id, "link_fullname": link_fullname},
)
def delete(self):
"""Delete this collection.
Example usage:
.. code-block:: python
reddit.subreddit("test").collections("some_uuid").mod.delete()
.. seealso::
:meth:`~.SubredditCollectionsModeration.create`
"""
self._reddit.post(
API_PATH["collection_delete"], data={"collection_id": self.collection_id}
)
def remove_post(self, submission: praw.models.Submission):
"""Remove a post from the collection.
:param submission: The post to remove, a :class:`.Submission`, its permalink as
a ``str``, its fullname as a ``str``, or its ID as a ``str``.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.remove_post("bgibu9")
.. seealso::
:meth:`.add_post`
"""
link_fullname = self._post_fullname(submission)
self._reddit.post(
API_PATH["collection_remove_post"],
data={"collection_id": self.collection_id, "link_fullname": link_fullname},
)
def reorder(self, links: list[str | praw.models.Submission]):
r"""Reorder posts in the collection.
:param links: A list of :class:`.Submission`\ s or a ``str`` that is either a
fullname or an ID.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
current_order = collection.link_ids
new_order = reversed(current_order)
collection.mod.reorder(new_order)
"""
link_ids = ",".join(self._post_fullname(post) for post in links)
self._reddit.post(
API_PATH["collection_reorder"],
data={"collection_id": self.collection_id, "link_ids": link_ids},
)
def update_description(self, description: str):
"""Update the collection's description.
:param description: The new description.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.update_description("Please enjoy these links!")
.. seealso::
:meth:`.update_title`
"""
self._reddit.post(
API_PATH["collection_desc"],
data={"collection_id": self.collection_id, "description": description},
)
def update_display_layout(self, display_layout: str):
"""Update the collection's display layout.
:param display_layout: Either ``"TIMELINE"`` for events or discussions or
``"GALLERY"`` for images or memes. Passing ``None`` will clear the set
layout and ``collection.display_layout`` will be ``None``, however, the
collection will appear on Reddit as if ``display_layout`` is set to
``"TIMELINE"``.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.update_display_layout("GALLERY")
"""
self._reddit.post(
API_PATH["collection_layout"],
data={
"collection_id": self.collection_id,
"display_layout": display_layout,
},
)
def update_title(self, title: str):
"""Update the collection's title.
:param title: The new title.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.update_title("Titley McTitleface")
.. seealso::
:meth:`.update_description`
"""
self._reddit.post(
API_PATH["collection_title"],
data={"collection_id": self.collection_id, "title": title},
)
class SubredditCollectionsModeration(PRAWBase):
r"""Class to represent moderator actions on a :class:`.Subreddit`'s :class:`.Collection`\ s.
Obtain an instance via:
.. code-block:: python
reddit.subreddit("test").collections.mod
"""
def __init__(
self,
reddit: praw.Reddit,
sub_fullname: str,
_data: dict[str, Any] | None = None,
):
"""Initialize a :class:`.SubredditCollectionsModeration` instance."""
super().__init__(reddit, _data)
self.subreddit_fullname = sub_fullname
@_deprecate_args("title", "description", "display_layout")
def create(
self, *, description: str, display_layout: str | None = None, title: str
) -> Collection:
"""Create a new :class:`.Collection`.
The authenticated account must have appropriate moderator permissions in the
subreddit this collection belongs to.
:param description: The description, up to 500 characters.
:param display_layout: Either ``"TIMELINE"`` for events or discussions or
``"GALLERY"`` for images or memes. Passing ``""`` or ``None`` will make the
collection appear on Reddit as if this is set to ``"TIMELINE"`` (default:
``None``).
:param title: The title of the collection, up to 300 characters.
:returns: The newly created :class:`.Collection`.
Example usage:
.. code-block:: python
my_sub = reddit.subreddit("test")
new_collection = my_sub.collections.mod.create(title="Title", description="desc")
new_collection.mod.add_post("bgibu9")
To specify the display layout as ``"GALLERY"`` when creating the collection:
.. code-block:: python
my_sub = reddit.subreddit("test")
new_collection = my_sub.collections.mod.create(
title="Title", description="desc", display_layout="GALLERY"
)
new_collection.mod.add_post("bgibu9")
.. seealso::
:meth:`~.CollectionModeration.delete`
"""
data = {
"sr_fullname": self.subreddit_fullname,
"title": title,
"description": description,
}
if display_layout:
data["display_layout"] = display_layout
return self._reddit.post(
API_PATH["collection_create"],
data=data,
)
class SubredditCollections(PRAWBase):
r"""Class to represent a :class:`.Subreddit`'s :class:`.Collection`\ s.
Obtain an instance via:
.. code-block:: python
reddit.subreddit("test").collections
"""
@cachedproperty
def mod(self) -> SubredditCollectionsModeration:
"""Get an instance of :class:`.SubredditCollectionsModeration`.
Provides :meth:`~SubredditCollectionsModeration.create`:
.. code-block:: python
my_sub = reddit.subreddit("test")
new_collection = my_sub.collections.mod.create(title="Title", description="desc")
"""
return SubredditCollectionsModeration(self._reddit, self.subreddit.fullname)
def __call__(
self,
collection_id: str | None = None,
permalink: str | None = None,
) -> Collection:
"""Return the :class:`.Collection` with the specified ID.
:param collection_id: The ID of a :class:`.Collection` (default: ``None``).
:param permalink: The permalink of a collection (default: ``None``).
:returns: The specified :class:`.Collection`.
Exactly one of ``collection_id`` or ``permalink`` is required.
Example usage:
.. code-block:: python
subreddit = reddit.subreddit("test")
uuid = "847e4548-a3b5-4ad7-afb4-edbfc2ed0a6b"
collection = subreddit.collections(uuid)
print(collection.title)
print(collection.description)
permalink = "https://www.reddit.com/r/SUBREDDIT/collection/" + uuid
collection = subreddit.collections(permalink=permalink)
print(collection.title)
print(collection.description)
"""
if (collection_id is None) == (permalink is None):
msg = "Exactly one of 'collection_id' or 'permalink' must be provided."
raise TypeError(msg)
return Collection(
self._reddit, collection_id=collection_id, permalink=permalink
)
def __init__(
self,
reddit: praw.Reddit,
subreddit: praw.models.Subreddit,
_data: dict[str, Any] | None = None,
):
"""Initialize a :class:`.SubredditCollections` instance."""
super().__init__(reddit, _data)
self.subreddit = subreddit
def __iter__(self):
r"""Iterate over the :class:`.Subreddit`'s :class:`.Collection`\ s.
Example usage:
.. code-block:: python
for collection in reddit.subreddit("test").collections:
print(collection.permalink)
"""
request = self._reddit.get(
API_PATH["collection_subreddit"],
params={"sr_fullname": self.subreddit.fullname},
)
yield from request
class Collection(RedditBase):
"""Class to represent a :class:`.Collection`.
Obtain an instance via:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
or
.. code-block:: python
collection = reddit.subreddit("test").collections(
permalink="https://reddit.com/r/SUBREDDIT/collection/some_uuid"
)
.. include:: ../../typical_attributes.rst
=================== =============================================================
Attribute Description
=================== =============================================================
``author`` The :class:`.Redditor` who created the collection.
``collection_id`` The UUID of the collection.
``created_at_utc`` Time the collection was created, represented in `Unix Time`_.
``description`` The collection description.
``display_layout`` The collection display layout.
``last_update_utc`` Time the collection was last updated, represented in `Unix
Time`_.
``link_ids`` A list of :class:`.Submission` fullnames.
``permalink`` The collection's permalink (to view on the web).
``sorted_links`` An iterable listing of the posts in this collection.
``title`` The title of the collection.
=================== =============================================================
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
"""
STR_FIELD = "collection_id"
@cachedproperty
def mod(self) -> CollectionModeration:
"""Get an instance of :class:`.CollectionModeration`.
Provides access to various methods, including
:meth:`~.CollectionModeration.add_post`, :meth:`~.CollectionModeration.delete`,
:meth:`~.CollectionModeration.reorder`, and
:meth:`~.CollectionModeration.update_title`.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
collection.mod.update_title("My new title!")
"""
return CollectionModeration(self._reddit, self.collection_id)
@cachedproperty
def subreddit(self) -> praw.models.Subreddit:
"""Get the subreddit that this collection belongs to.
For example:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
subreddit = collection.subreddit
"""
return next(self._reddit.info(fullnames=[self.subreddit_id]))
def __init__(
self,
reddit: praw.Reddit,
_data: dict[str, Any] = None,
collection_id: str | None = None,
permalink: str | None = None,
):
"""Initialize a :class:`.Collection` instance.
:param reddit: An instance of :class:`.Reddit`.
:param _data: Any data associated with the :class:`.Collection`.
:param collection_id: The ID of the :class:`.Collection`.
:param permalink: The permalink of the :class:`.Collection`.
"""
if (_data, collection_id, permalink).count(None) != 2:
msg = "Exactly one of '_data', 'collection_id', or 'permalink' must be provided."
raise TypeError(msg)
if permalink:
collection_id = self._url_parts(permalink)[4]
if collection_id:
self.collection_id = collection_id # set from _data otherwise
super().__init__(reddit, _data)
self._info_params = {
"collection_id": self.collection_id,
"include_links": True,
}
def __iter__(self) -> Iterator:
"""Provide a way to iterate over the posts in this :class:`.Collection`.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
for submission in collection:
print(submission.title, submission.permalink)
"""
yield from self.sorted_links
def __len__(self) -> int:
"""Get the number of posts in this :class:`.Collection`.
Example usage:
.. code-block:: python
collection = reddit.subreddit("test").collections("some_uuid")
print(len(collection))
"""
return len(self.link_ids)
def __setattr__(self, attribute: str, value: Any):
"""Objectify author, subreddit, and sorted_links attributes."""
if attribute == "author_name":
self.author = self._reddit.redditor(value)
elif attribute == "sorted_links":
value = self._reddit._objector.objectify(value)
super().__setattr__(attribute, value)
def _fetch(self):
data = self._fetch_data()
try:
self._reddit._objector.check_error(data)
except ClientException:
# A well-formed but invalid Collections ID during fetch time
# causes Reddit to return something that looks like an error
# but with no content.
msg = f"Error during fetch. Check collection ID {self.collection_id!r} is correct."
raise ClientException(msg) from None
other = type(self)(self._reddit, _data=data)
self.__dict__.update(other.__dict__)
super()._fetch()
def _fetch_info(self):
return "collection", {}, self._info_params
def follow(self):
"""Follow this :class:`.Collection`.
Example usage:
.. code-block:: python
reddit.subreddit("test").collections("some_uuid").follow()
.. seealso::
:meth:`.unfollow`
"""
self._reddit.post(
API_PATH["collection_follow"],
data={"collection_id": self.collection_id, "follow": True},
)
def unfollow(self):
"""Unfollow this :class:`.Collection`.
Example usage:
.. code-block:: python
reddit.subreddit("test").collections("some_uuid").unfollow()
.. seealso::
:meth:`.follow`
"""
self._reddit.post(
API_PATH["collection_follow"],
data={"collection_id": self.collection_id, "follow": False},
)
Subreddit._subreddit_collections_class = SubredditCollections