Initial commit
This commit is contained in:
13
backend/venv/Lib/site-packages/praw/__init__.py
Normal file
13
backend/venv/Lib/site-packages/praw/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Python Reddit API Wrapper.
|
||||
|
||||
PRAW, an acronym for "Python Reddit API Wrapper", is a python package that allows for
|
||||
simple access to Reddit's API. PRAW aims to be as easy to use as possible and is
|
||||
designed to follow all of Reddit's API rules. You have to give a useragent, everything
|
||||
else is handled by PRAW so you needn't worry about violating them.
|
||||
|
||||
More information about PRAW can be found at https://github.com/praw-dev/praw.
|
||||
|
||||
"""
|
||||
|
||||
from .const import __version__
|
||||
from .reddit import Reddit
|
||||
178
backend/venv/Lib/site-packages/praw/config.py
Normal file
178
backend/venv/Lib/site-packages/praw/config.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""Provides the code to load PRAW's configuration file ``praw.ini``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import configparser
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
from .exceptions import ClientException
|
||||
|
||||
|
||||
class _NotSet:
|
||||
def __bool__(self) -> bool:
|
||||
return False
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "NotSet"
|
||||
|
||||
|
||||
class Config:
|
||||
"""A class containing the configuration for a Reddit site."""
|
||||
|
||||
CONFIG = None
|
||||
CONFIG_NOT_SET = _NotSet() # Represents a config value that is not set.
|
||||
LOCK = Lock()
|
||||
INTERPOLATION_LEVEL = {
|
||||
"basic": configparser.BasicInterpolation,
|
||||
"extended": configparser.ExtendedInterpolation,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _config_boolean(item: bool | str) -> bool:
|
||||
if isinstance(item, bool):
|
||||
return item
|
||||
return item.lower() in {"1", "yes", "true", "on"}
|
||||
|
||||
@classmethod
|
||||
def _load_config(cls, *, config_interpolation: str | None = None):
|
||||
"""Attempt to load settings from various praw.ini files."""
|
||||
if config_interpolation is not None:
|
||||
interpolator_class = cls.INTERPOLATION_LEVEL[config_interpolation]()
|
||||
else:
|
||||
interpolator_class = None
|
||||
|
||||
config = configparser.ConfigParser(interpolation=interpolator_class)
|
||||
module_dir = Path(sys.modules[__name__].__file__).parent
|
||||
|
||||
if "APPDATA" in os.environ: # Windows
|
||||
os_config_path = Path(os.environ["APPDATA"])
|
||||
elif "XDG_CONFIG_HOME" in os.environ: # Modern Linux
|
||||
os_config_path = Path(os.environ["XDG_CONFIG_HOME"])
|
||||
elif "HOME" in os.environ: # Legacy Linux
|
||||
os_config_path = Path(os.environ["HOME"]) / ".config"
|
||||
else:
|
||||
os_config_path = None
|
||||
|
||||
locations = [str(module_dir / "praw.ini"), "praw.ini"]
|
||||
|
||||
if os_config_path is not None:
|
||||
locations.insert(1, str(os_config_path / "praw.ini"))
|
||||
|
||||
config.read(locations)
|
||||
cls.CONFIG = config
|
||||
|
||||
@property
|
||||
def short_url(self) -> str:
|
||||
"""Return the short url.
|
||||
|
||||
:raises: :class:`.ClientException` if it is not set.
|
||||
|
||||
"""
|
||||
if self._short_url is self.CONFIG_NOT_SET:
|
||||
msg = "No short domain specified."
|
||||
raise ClientException(msg)
|
||||
return self._short_url
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
site_name: str,
|
||||
config_interpolation: str | None = None,
|
||||
**settings: str,
|
||||
):
|
||||
"""Initialize a :class:`.Config` instance."""
|
||||
with Config.LOCK:
|
||||
if Config.CONFIG is None:
|
||||
self._load_config(config_interpolation=config_interpolation)
|
||||
|
||||
self._settings = settings
|
||||
self.custom = dict(Config.CONFIG.items(site_name), **settings)
|
||||
|
||||
self.client_id = self.client_secret = self.oauth_url = None
|
||||
self.reddit_url = self.refresh_token = self.redirect_uri = None
|
||||
self.password = self.user_agent = self.username = None
|
||||
|
||||
self._initialize_attributes()
|
||||
|
||||
def _fetch(self, key: str) -> Any:
|
||||
value = self.custom[key]
|
||||
del self.custom[key]
|
||||
return value
|
||||
|
||||
def _fetch_default(
|
||||
self, key: str, *, default: bool | float | str | None = None
|
||||
) -> Any:
|
||||
if key not in self.custom:
|
||||
return default
|
||||
return self._fetch(key)
|
||||
|
||||
def _fetch_or_not_set(self, key: str) -> Any | _NotSet:
|
||||
if key in self._settings: # Passed in values have the highest priority
|
||||
return self._fetch(key)
|
||||
|
||||
env_value = os.getenv(f"praw_{key}")
|
||||
ini_value = self._fetch_default(key) # Needed to remove from custom
|
||||
|
||||
# Environment variables have higher priority than praw.ini settings
|
||||
return env_value or ini_value or self.CONFIG_NOT_SET
|
||||
|
||||
def _initialize_attributes(self):
|
||||
self._short_url = self._fetch_default("short_url") or self.CONFIG_NOT_SET
|
||||
self.check_for_async = self._config_boolean(
|
||||
self._fetch_default("check_for_async", default=True)
|
||||
)
|
||||
self.check_for_updates = self._config_boolean(
|
||||
self._fetch_or_not_set("check_for_updates")
|
||||
)
|
||||
self.warn_comment_sort = self._config_boolean(
|
||||
self._fetch_default("warn_comment_sort", default=True)
|
||||
)
|
||||
self.warn_additional_fetch_params = self._config_boolean(
|
||||
self._fetch_default("warn_additional_fetch_params", default=True)
|
||||
)
|
||||
self.window_size = self._fetch_default("window_size", default=600)
|
||||
self.kinds = {
|
||||
x: self._fetch(f"{x}_kind")
|
||||
for x in [
|
||||
"comment",
|
||||
"message",
|
||||
"redditor",
|
||||
"submission",
|
||||
"subreddit",
|
||||
"trophy",
|
||||
]
|
||||
}
|
||||
|
||||
for attribute in (
|
||||
"client_id",
|
||||
"client_secret",
|
||||
"redirect_uri",
|
||||
"refresh_token",
|
||||
"password",
|
||||
"user_agent",
|
||||
"username",
|
||||
):
|
||||
setattr(self, attribute, self._fetch_or_not_set(attribute))
|
||||
|
||||
for required_attribute in (
|
||||
"oauth_url",
|
||||
"ratelimit_seconds",
|
||||
"reddit_url",
|
||||
"timeout",
|
||||
):
|
||||
setattr(self, required_attribute, self._fetch(required_attribute))
|
||||
|
||||
for attribute, conversion in {
|
||||
"ratelimit_seconds": int,
|
||||
"timeout": int,
|
||||
}.items():
|
||||
try:
|
||||
setattr(self, attribute, conversion(getattr(self, attribute)))
|
||||
except ValueError:
|
||||
msg = f"An incorrect config type was given for option {attribute}. The expected type is {conversion.__name__}, but the given value is {getattr(self, attribute)}."
|
||||
raise ValueError(msg) from None
|
||||
14
backend/venv/Lib/site-packages/praw/const.py
Normal file
14
backend/venv/Lib/site-packages/praw/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""PRAW constants."""
|
||||
|
||||
from .endpoints import API_PATH # noqa: F401
|
||||
|
||||
__version__ = "7.8.1"
|
||||
|
||||
USER_AGENT_FORMAT = f"{{}} PRAW/{__version__}"
|
||||
|
||||
MAX_IMAGE_SIZE = 512000
|
||||
MIN_JPEG_SIZE = 128
|
||||
MIN_PNG_SIZE = 67
|
||||
|
||||
JPEG_HEADER = b"\xff\xd8\xff"
|
||||
PNG_HEADER = b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"
|
||||
235
backend/venv/Lib/site-packages/praw/endpoints.py
Normal file
235
backend/venv/Lib/site-packages/praw/endpoints.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""List of API endpoints PRAW knows about."""
|
||||
|
||||
# fmt: off
|
||||
API_PATH = {
|
||||
"about_edited": "r/{subreddit}/about/edited/",
|
||||
"about_log": "r/{subreddit}/about/log/",
|
||||
"about_modqueue": "r/{subreddit}/about/modqueue/",
|
||||
"about_reports": "r/{subreddit}/about/reports/",
|
||||
"about_spam": "r/{subreddit}/about/spam/",
|
||||
"about_sticky": "r/{subreddit}/about/sticky/",
|
||||
"about_stylesheet": "r/{subreddit}/about/stylesheet/",
|
||||
"about_traffic": "r/{subreddit}/about/traffic/",
|
||||
"about_unmoderated": "r/{subreddit}/about/unmoderated/",
|
||||
"accept_mod_invite": "r/{subreddit}/api/accept_moderator_invite",
|
||||
"add_subreddit_rule": "api/add_subreddit_rule",
|
||||
"add_whitelisted": "api/add_whitelisted",
|
||||
"approve": "api/approve/",
|
||||
"award_thing": "api/v2/gold/gild",
|
||||
"block": "api/block",
|
||||
"block_user": "/api/block_user/",
|
||||
"blocked": "prefs/blocked/",
|
||||
"collapse": "api/collapse_message/",
|
||||
"collection": "api/v1/collections/collection",
|
||||
"collection_add_post": "api/v1/collections/add_post_to_collection",
|
||||
"collection_create": "api/v1/collections/create_collection",
|
||||
"collection_delete": "api/v1/collections/delete_collection",
|
||||
"collection_desc": "api/v1/collections/update_collection_description",
|
||||
"collection_layout": "api/v1/collections/update_collection_display_layout",
|
||||
"collection_follow": "api/v1/collections/follow_collection",
|
||||
"collection_remove_post": "api/v1/collections/remove_post_in_collection",
|
||||
"collection_reorder": "api/v1/collections/reorder_collection",
|
||||
"collection_subreddit": "api/v1/collections/subreddit_collections",
|
||||
"collection_title": "api/v1/collections/update_collection_title",
|
||||
"comment": "api/comment/",
|
||||
"comment_replies": "message/comments/",
|
||||
"compose": "api/compose/",
|
||||
"contest_mode": "api/set_contest_mode/",
|
||||
"convert_rte_body": "api/convert_rte_body_format",
|
||||
"del": "api/del/",
|
||||
"delete_message": "api/del_msg",
|
||||
"delete_sr_banner": "r/{subreddit}/api/delete_sr_banner",
|
||||
"delete_sr_header": "r/{subreddit}/api/delete_sr_header",
|
||||
"delete_sr_icon": "r/{subreddit}/api/delete_sr_icon",
|
||||
"delete_sr_image": "r/{subreddit}/api/delete_sr_img",
|
||||
"deleteflair": "r/{subreddit}/api/deleteflair",
|
||||
"distinguish": "api/distinguish/",
|
||||
"draft": "api/v1/draft",
|
||||
"drafts": "api/v1/drafts",
|
||||
"domain": "domain/{domain}/",
|
||||
"duplicates": "duplicates/{submission_id}/",
|
||||
"edit": "api/editusertext/",
|
||||
"emoji_delete": "api/v1/{subreddit}/emoji/{emoji_name}",
|
||||
"emoji_lease": "api/v1/{subreddit}/emoji_asset_upload_s3.json",
|
||||
"emoji_list": "api/v1/{subreddit}/emojis/all",
|
||||
"emoji_update": "api/v1/{subreddit}/emoji_permissions",
|
||||
"emoji_upload": "api/v1/{subreddit}/emoji.json",
|
||||
"flair": "r/{subreddit}/api/flair/",
|
||||
"flairconfig": "r/{subreddit}/api/flairconfig/",
|
||||
"flaircsv": "r/{subreddit}/api/flaircsv/",
|
||||
"flairlist": "r/{subreddit}/api/flairlist/",
|
||||
"flairselector": "r/{subreddit}/api/flairselector/",
|
||||
"flairtemplate_v2": "r/{subreddit}/api/flairtemplate_v2",
|
||||
"flairtemplateclear": "r/{subreddit}/api/clearflairtemplates/",
|
||||
"flairtemplatedelete": "r/{subreddit}/api/deleteflairtemplate/",
|
||||
"flairtemplatereorder": "r/{subreddit}/api/flair_template_order",
|
||||
"friend": "r/{subreddit}/api/friend/",
|
||||
"friend_v1": "api/v1/me/friends/{user}",
|
||||
"friends": "api/v1/me/friends/",
|
||||
"gild_user": "api/v1/gold/give/{username}/",
|
||||
"hide": "api/hide/",
|
||||
"ignore_reports": "api/ignore_reports/",
|
||||
"inbox": "message/inbox/",
|
||||
"info": "api/info/",
|
||||
"karma": "api/v1/me/karma",
|
||||
"leavecontributor": "api/leavecontributor",
|
||||
"link_flair": "r/{subreddit}/api/link_flair_v2",
|
||||
"list_banned": "r/{subreddit}/about/banned/",
|
||||
"list_contributor": "r/{subreddit}/about/contributors/",
|
||||
"list_moderator": "r/{subreddit}/about/moderators/",
|
||||
"list_invited_moderator": "/api/v1/{subreddit}/moderators_invited",
|
||||
"list_muted": "r/{subreddit}/about/muted/",
|
||||
"list_wikibanned": "r/{subreddit}/about/wikibanned/",
|
||||
"list_wikicontributor": "r/{subreddit}/about/wikicontributors/",
|
||||
"live_accept_invite": "api/live/{id}/accept_contributor_invite",
|
||||
"live_add_update": "api/live/{id}/update",
|
||||
"live_close": "api/live/{id}/close_thread",
|
||||
"live_contributors": "live/{id}/contributors",
|
||||
"live_discussions": "live/{id}/discussions",
|
||||
"live_focus": "live/{thread_id}/updates/{update_id}",
|
||||
"live_info": "api/live/by_id/{ids}",
|
||||
"live_invite": "api/live/{id}/invite_contributor",
|
||||
"live_leave": "api/live/{id}/leave_contributor",
|
||||
"live_now": "api/live/happening_now",
|
||||
"live_remove_contrib": "api/live/{id}/rm_contributor",
|
||||
"live_remove_invite": "api/live/{id}/rm_contributor_invite",
|
||||
"live_remove_update": "api/live/{id}/delete_update",
|
||||
"live_report": "api/live/{id}/report",
|
||||
"live_strike": "api/live/{id}/strike_update",
|
||||
"live_update_perms": "api/live/{id}/set_contributor_permissions",
|
||||
"live_update_thread": "api/live/{id}/edit",
|
||||
"live_updates": "live/{id}",
|
||||
"liveabout": "api/live/{id}/about/",
|
||||
"livecreate": "api/live/create",
|
||||
"lock": "api/lock/",
|
||||
"marknsfw": "api/marknsfw/",
|
||||
"me": "api/v1/me",
|
||||
"media_asset": "api/media/asset.json",
|
||||
"mentions": "message/mentions",
|
||||
"message": "message/messages/{id}/",
|
||||
"messages": "message/messages/",
|
||||
"mod_notes": "api/mod/notes",
|
||||
"mod_notes_bulk": "api/mod/notes/recent",
|
||||
"moderated": "user/{user}/moderated_subreddits/",
|
||||
"moderator_messages": "r/{subreddit}/message/moderator/",
|
||||
"moderator_unread": "r/{subreddit}/message/moderator/unread/",
|
||||
"modmail_archive": "api/mod/conversations/{id}/archive",
|
||||
"modmail_bulk_read": "api/mod/conversations/bulk/read",
|
||||
"modmail_conversation": "api/mod/conversations/{id}",
|
||||
"modmail_conversations": "api/mod/conversations/",
|
||||
"modmail_highlight": "api/mod/conversations/{id}/highlight",
|
||||
"modmail_mute": "api/mod/conversations/{id}/mute",
|
||||
"modmail_read": "api/mod/conversations/read",
|
||||
"modmail_subreddits": "api/mod/conversations/subreddits",
|
||||
"modmail_unarchive": "api/mod/conversations/{id}/unarchive",
|
||||
"modmail_unmute": "api/mod/conversations/{id}/unmute",
|
||||
"modmail_unread": "api/mod/conversations/unread",
|
||||
"modmail_unread_count": "api/mod/conversations/unread/count",
|
||||
"morechildren": "api/morechildren/",
|
||||
"multireddit": "user/{user}/m/{multi}/",
|
||||
"multireddit_api": "api/multi/user/{user}/m/{multi}/",
|
||||
"multireddit_base": "api/multi/",
|
||||
"multireddit_copy": "api/multi/copy/",
|
||||
"multireddit_rename": "api/multi/rename/",
|
||||
"multireddit_update": "api/multi/user/{user}/m/{multi}/r/{subreddit}",
|
||||
"multireddit_user": "api/multi/user/{user}/",
|
||||
"mute_sender": "api/mute_message_author/",
|
||||
"my_contributor": "subreddits/mine/contributor/",
|
||||
"my_moderator": "subreddits/mine/moderator/",
|
||||
"my_multireddits": "api/multi/mine/",
|
||||
"my_subreddits": "subreddits/mine/subscriber/",
|
||||
"post_requirements": "api/v1/{subreddit}/post_requirements",
|
||||
"preferences": "api/v1/me/prefs",
|
||||
"quarantine_opt_in": "api/quarantine_optin",
|
||||
"quarantine_opt_out": "api/quarantine_optout",
|
||||
"read_all_messages": "api/read_all_messages",
|
||||
"read_message": "api/read_message/",
|
||||
"removal_comment_message": "api/v1/modactions/removal_comment_message",
|
||||
"removal_link_message": "api/v1/modactions/removal_link_message",
|
||||
"removal_reasons": "api/v1/modactions/removal_reasons",
|
||||
"removal_reason": "api/v1/{subreddit}/removal_reasons/{id}",
|
||||
"removal_reasons_list": "api/v1/{subreddit}/removal_reasons",
|
||||
"remove_subreddit_rule": "api/remove_subreddit_rule",
|
||||
"remove_whitelisted": "api/remove_whitelisted",
|
||||
"remove": "api/remove/",
|
||||
"reorder_subreddit_rules": "api/reorder_subreddit_rules",
|
||||
"report": "api/report/",
|
||||
"rules": "r/{subreddit}/about/rules",
|
||||
"save": "api/save/",
|
||||
"search": "r/{subreddit}/search/",
|
||||
"select_flair": "r/{subreddit}/api/selectflair/",
|
||||
"sendreplies": "api/sendreplies",
|
||||
"sent": "message/sent/",
|
||||
"set_original_content": "api/set_original_content",
|
||||
"setpermissions": "r/{subreddit}/api/setpermissions/",
|
||||
"show_comment": "api/show_comment",
|
||||
"site_admin": "api/site_admin/",
|
||||
"spoiler": "api/spoiler/",
|
||||
"sticky_submission": "api/set_subreddit_sticky/",
|
||||
"store_visits": "api/store_visits",
|
||||
"structured_styles": "api/v1/structured_styles/{subreddit}",
|
||||
"style_asset_lease": "api/v1/style_asset_upload_s3/{subreddit}",
|
||||
"sub_recommended": "api/recommend/sr/{subreddits}",
|
||||
"submission": "comments/{id}/",
|
||||
"submission_replies": "message/selfreply/",
|
||||
"submit": "api/submit/",
|
||||
"submit_gallery_post": "api/submit_gallery_post.json",
|
||||
"submit_poll_post": "api/submit_poll_post",
|
||||
"subreddit": "r/{subreddit}/",
|
||||
"subreddit_about": "r/{subreddit}/about/",
|
||||
"subreddit_filter": "api/filter/user/{user}/f/{special}/r/{subreddit}",
|
||||
"subreddit_filter_list": "api/filter/user/{user}/f/{special}",
|
||||
"subreddit_random": "r/{subreddit}/random/",
|
||||
"subreddit_settings": "r/{subreddit}/about/edit/",
|
||||
"subreddit_stylesheet": "r/{subreddit}/api/subreddit_stylesheet/",
|
||||
"subreddits_by_topic": "api/subreddits_by_topic",
|
||||
"subreddits_default": "subreddits/default/",
|
||||
"subreddits_gold": "subreddits/premium/",
|
||||
"subreddits_name_search": "api/search_reddit_names/",
|
||||
"subreddits_new": "subreddits/new/",
|
||||
"subreddits_popular": "subreddits/popular/",
|
||||
"subreddits_search": "subreddits/search/",
|
||||
"subscribe": "api/subscribe/",
|
||||
"suggested_sort": "api/set_suggested_sort/",
|
||||
"trophies": "api/v1/user/{user}/trophies",
|
||||
"trusted": "prefs/trusted",
|
||||
"unblock_subreddit": "api/unblock_subreddit",
|
||||
"uncollapse": "api/uncollapse_message/",
|
||||
"unfriend": "r/{subreddit}/api/unfriend/",
|
||||
"unhide": "api/unhide/",
|
||||
"unignore_reports": "api/unignore_reports/",
|
||||
"unlock": "api/unlock/",
|
||||
"unmarknsfw": "api/unmarknsfw/",
|
||||
"unmute_sender": "api/unmute_message_author/",
|
||||
"unread": "message/unread/",
|
||||
"unread_message": "api/unread_message/",
|
||||
"unsave": "api/unsave/",
|
||||
"unspoiler": "api/unspoiler/",
|
||||
"update_crowd_control": "api/update_crowd_control_level",
|
||||
"update_settings": "api/v1/subreddit/update_settings",
|
||||
"update_subreddit_rule": "api/update_subreddit_rule",
|
||||
"upload_image": "r/{subreddit}/api/upload_sr_img",
|
||||
"user": "user/{user}/",
|
||||
"user_about": "user/{user}/about/",
|
||||
"user_by_fullname": "/api/user_data_by_account_ids",
|
||||
"user_flair": "r/{subreddit}/api/user_flair_v2",
|
||||
"username_available": "api/username_available",
|
||||
"users_new": "users/new",
|
||||
"users_popular": "users/popular",
|
||||
"users_search": "users/search",
|
||||
"vote": "api/vote/",
|
||||
"widget_create": "r/{subreddit}/api/widget",
|
||||
"widget_lease": "r/{subreddit}/api/widget_image_upload_s3",
|
||||
"widget_modify": "r/{subreddit}/api/widget/{widget_id}",
|
||||
"widget_order": "r/{subreddit}/api/widget_order/{section}",
|
||||
"widgets": "r/{subreddit}/api/widgets",
|
||||
"wiki_discussions": "r/{subreddit}/wiki/discussions/{page}",
|
||||
"wiki_edit": "r/{subreddit}/api/wiki/edit",
|
||||
"wiki_page": "r/{subreddit}/wiki/{page}",
|
||||
"wiki_page_editor": "r/{subreddit}/api/wiki/alloweditor/{method}",
|
||||
"wiki_page_revisions": "r/{subreddit}/wiki/revisions/{page}",
|
||||
"wiki_page_settings": "r/{subreddit}/wiki/settings/{page}",
|
||||
"wiki_pages": "r/{subreddit}/wiki/pages/",
|
||||
"wiki_revert": "r/{subreddit}/api/wiki/revert",
|
||||
"wiki_revisions": "r/{subreddit}/wiki/revisions/",
|
||||
}
|
||||
307
backend/venv/Lib/site-packages/praw/exceptions.py
Normal file
307
backend/venv/Lib/site-packages/praw/exceptions.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""PRAW exception classes.
|
||||
|
||||
Includes two main exceptions: :class:`.RedditAPIException` for when something goes wrong
|
||||
on the server side, and :class:`.ClientException` when something goes wrong on the
|
||||
client side. Both of these classes extend :class:`.PRAWException`.
|
||||
|
||||
All other exceptions are subclassed from :class:`.ClientException`.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from warnings import warn
|
||||
|
||||
from .util import _deprecate_args
|
||||
|
||||
|
||||
class PRAWException(Exception):
|
||||
"""The base PRAW Exception that all other exception classes extend."""
|
||||
|
||||
|
||||
class RedditErrorItem:
|
||||
"""Represents a single error returned from Reddit's API."""
|
||||
|
||||
@property
|
||||
def error_message(self) -> str:
|
||||
"""Get the completed error message string."""
|
||||
error_str = self.error_type
|
||||
if self.message:
|
||||
error_str += f": {self.message!r}"
|
||||
if self.field:
|
||||
error_str += f" on field {self.field!r}"
|
||||
return error_str
|
||||
|
||||
def __eq__(self, other: RedditErrorItem | list[str]) -> bool:
|
||||
"""Check for equality."""
|
||||
if isinstance(other, RedditErrorItem):
|
||||
return (self.error_type, self.message, self.field) == (
|
||||
other.error_type,
|
||||
other.message,
|
||||
other.field,
|
||||
)
|
||||
return super().__eq__(other)
|
||||
|
||||
@_deprecate_args("error_type", "message", "field")
|
||||
def __init__(
|
||||
self,
|
||||
error_type: str,
|
||||
*,
|
||||
field: str | None = None,
|
||||
message: str | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.RedditErrorItem` instance.
|
||||
|
||||
:param error_type: The error type set on Reddit's end.
|
||||
:param field: The input field associated with the error, if available.
|
||||
:param message: The associated message for the error.
|
||||
|
||||
"""
|
||||
self.error_type = error_type
|
||||
self.message = message
|
||||
self.field = field
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return (
|
||||
f"{self.__class__.__name__}(error_type={self.error_type!r},"
|
||||
f" message={self.message!r}, field={self.field!r})"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Get the message returned from str(self)."""
|
||||
return self.error_message
|
||||
|
||||
|
||||
class ClientException(PRAWException):
|
||||
"""Indicate exceptions that don't involve interaction with Reddit's API."""
|
||||
|
||||
|
||||
class DuplicateReplaceException(ClientException):
|
||||
"""Indicate exceptions that involve the replacement of :class:`.MoreComments`."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a :class:`.DuplicateReplaceException` instance."""
|
||||
super().__init__(
|
||||
"A duplicate comment has been detected. Are you attempting to call"
|
||||
" 'replace_more_comments' more than once?"
|
||||
)
|
||||
|
||||
|
||||
class InvalidFlairTemplateID(ClientException):
|
||||
"""Indicate exceptions where an invalid flair template ID is given."""
|
||||
|
||||
def __init__(self, template_id: str):
|
||||
"""Initialize an :class:`.InvalidFlairTemplateID` instance."""
|
||||
super().__init__(
|
||||
f"The flair template ID '{template_id}' is invalid. If you are trying to"
|
||||
" create a flair, please use the 'add' method."
|
||||
)
|
||||
|
||||
|
||||
class InvalidImplicitAuth(ClientException):
|
||||
"""Indicate exceptions where an implicit auth type is used incorrectly."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an :class:`.InvalidImplicitAuth` instance."""
|
||||
super().__init__("Implicit authorization can only be used with installed apps.")
|
||||
|
||||
|
||||
class InvalidURL(ClientException):
|
||||
"""Indicate exceptions where an invalid URL is entered."""
|
||||
|
||||
@_deprecate_args("url", "message")
|
||||
def __init__(self, url: str, *, message: str = "Invalid URL: {}"):
|
||||
"""Initialize an :class:`.InvalidURL` instance.
|
||||
|
||||
:param url: The invalid URL.
|
||||
:param message: The message to display. Must contain a format identifier (``{}``
|
||||
or ``{0}``) (default: ``"Invalid URL: {}"``).
|
||||
|
||||
"""
|
||||
super().__init__(message.format(url))
|
||||
|
||||
|
||||
class MissingRequiredAttributeException(ClientException):
|
||||
"""Indicate exceptions caused by not including a required attribute."""
|
||||
|
||||
|
||||
class ReadOnlyException(ClientException):
|
||||
"""Raised when a method call requires :attr:`.read_only` mode to be disabled."""
|
||||
|
||||
|
||||
class TooLargeMediaException(ClientException):
|
||||
"""Indicate exceptions from uploading media that's too large."""
|
||||
|
||||
@_deprecate_args("maximum_size", "actual")
|
||||
def __init__(self, *, actual: int, maximum_size: int):
|
||||
"""Initialize a :class:`.TooLargeMediaException` instance.
|
||||
|
||||
:param actual: The actual size of the uploaded media.
|
||||
:param maximum_size: The maximum size of the uploaded media.
|
||||
|
||||
"""
|
||||
self.maximum_size = maximum_size
|
||||
self.actual = actual
|
||||
super().__init__(
|
||||
f"The media that you uploaded was too large (maximum size is {maximum_size}"
|
||||
f" bytes, uploaded {actual} bytes)"
|
||||
)
|
||||
|
||||
|
||||
class WebSocketException(ClientException):
|
||||
"""Indicate exceptions caused by use of WebSockets."""
|
||||
|
||||
@property
|
||||
def original_exception(self) -> Exception:
|
||||
"""Access the ``original_exception`` attribute (now deprecated)."""
|
||||
warn(
|
||||
"Accessing the attribute 'original_exception' is deprecated. Please rewrite"
|
||||
" your code in such a way that this attribute does not need to be used. It"
|
||||
" will be removed in PRAW 8.0.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._original_exception
|
||||
|
||||
@original_exception.setter
|
||||
def original_exception(self, value: Exception):
|
||||
self._original_exception = value
|
||||
|
||||
@original_exception.deleter
|
||||
def original_exception(self):
|
||||
del self._original_exception
|
||||
|
||||
def __init__(self, message: str, exception: Exception | None):
|
||||
"""Initialize a :class:`.WebSocketException` instance.
|
||||
|
||||
:param message: The exception message.
|
||||
:param exception: The exception thrown by the websocket library.
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is deprecated. It will be removed in PRAW 8.0.
|
||||
|
||||
"""
|
||||
super().__init__(message)
|
||||
self._original_exception = exception
|
||||
|
||||
|
||||
class MediaPostFailed(WebSocketException):
|
||||
"""Indicate exceptions where media uploads failed.."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a :class:`.MediaPostFailed` instance."""
|
||||
super().__init__(
|
||||
"The attempted media upload action has failed. Possible causes include the"
|
||||
" corruption of media files. Check that the media file can be opened on"
|
||||
" your local machine.",
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
class APIException(PRAWException):
|
||||
"""Old class preserved for alias purposes.
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Class :class:`.APIException` has been deprecated in favor of
|
||||
:class:`.RedditAPIException`. This class will be removed in PRAW 8.0.
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def parse_exception_list(
|
||||
exceptions: list[RedditErrorItem | list[str]],
|
||||
) -> list[RedditErrorItem]:
|
||||
"""Covert an exception list into a :class:`.RedditErrorItem` list."""
|
||||
return [
|
||||
(
|
||||
exception
|
||||
if isinstance(exception, RedditErrorItem)
|
||||
else RedditErrorItem(
|
||||
error_type=exception[0],
|
||||
field=exception[2] if bool(exception[2]) else "",
|
||||
message=exception[1] if bool(exception[1]) else "",
|
||||
)
|
||||
)
|
||||
for exception in exceptions
|
||||
]
|
||||
|
||||
@property
|
||||
def error_type(self) -> str:
|
||||
"""Get error_type.
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Accessing attributes through instances of :class:`.RedditAPIException` is
|
||||
deprecated. This behavior will be removed in PRAW 8.0. Check out the
|
||||
:ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
|
||||
from this behavior.
|
||||
|
||||
"""
|
||||
return self._get_old_attr("error_type")
|
||||
|
||||
@property
|
||||
def field(self) -> str:
|
||||
"""Get field.
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Accessing attributes through instances of :class:`.RedditAPIException` is
|
||||
deprecated. This behavior will be removed in PRAW 8.0. Check out the
|
||||
:ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
|
||||
from this behavior.
|
||||
|
||||
"""
|
||||
return self._get_old_attr("field")
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
"""Get message.
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Accessing attributes through instances of :class:`.RedditAPIException` is
|
||||
deprecated. This behavior will be removed in PRAW 8.0. Check out the
|
||||
:ref:`PRAW 7 Migration tutorial <Exception_Handling>` on how to migrate code
|
||||
from this behavior.
|
||||
|
||||
"""
|
||||
return self._get_old_attr("message")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
items: list[RedditErrorItem | list[str] | str] | str,
|
||||
*optional_args: str,
|
||||
):
|
||||
"""Initialize a :class:`.RedditAPIException` instance.
|
||||
|
||||
:param items: Either a list of instances of :class:`.RedditErrorItem` or a list
|
||||
containing lists of unformed errors.
|
||||
:param optional_args: Takes the second and third arguments that
|
||||
:class:`.APIException` used to take.
|
||||
|
||||
"""
|
||||
if isinstance(items, str):
|
||||
items = [[items, *optional_args]]
|
||||
elif isinstance(items, list) and isinstance(items[0], str):
|
||||
items = [items]
|
||||
self.items = self.parse_exception_list(items)
|
||||
super().__init__(*self.items)
|
||||
|
||||
def _get_old_attr(self, attrname: str) -> Any:
|
||||
warn(
|
||||
f"Accessing attribute '{attrname}' through APIException is deprecated."
|
||||
" This behavior will be removed in PRAW 8.0. Check out"
|
||||
" https://praw.readthedocs.io/en/latest/package_info/praw7_migration.html"
|
||||
" to learn how to migrate your code.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return getattr(self.items[0], attrname)
|
||||
|
||||
|
||||
class RedditAPIException(APIException):
|
||||
"""Container for error messages from Reddit's API."""
|
||||
BIN
backend/venv/Lib/site-packages/praw/images/PRAW logo.png
Normal file
BIN
backend/venv/Lib/site-packages/praw/images/PRAW logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
65
backend/venv/Lib/site-packages/praw/models/__init__.py
Normal file
65
backend/venv/Lib/site-packages/praw/models/__init__.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""Provide the PRAW models."""
|
||||
|
||||
from .auth import Auth
|
||||
from .front import Front
|
||||
from .helpers import DraftHelper, LiveHelper, MultiredditHelper, SubredditHelper
|
||||
from .inbox import Inbox
|
||||
from .list.draft import DraftList
|
||||
from .list.moderated import ModeratedList
|
||||
from .list.redditor import RedditorList
|
||||
from .list.trophy import TrophyList
|
||||
from .listing.domain import DomainListing
|
||||
from .listing.generator import ListingGenerator
|
||||
from .listing.listing import Listing, ModeratorListing, ModmailConversationsListing
|
||||
from .mod_action import ModAction
|
||||
from .mod_note import ModNote
|
||||
from .mod_notes import RedditModNotes, RedditorModNotes, SubredditModNotes
|
||||
from .preferences import Preferences
|
||||
from .reddit.collections import Collection
|
||||
from .reddit.comment import Comment
|
||||
from .reddit.draft import Draft
|
||||
from .reddit.emoji import Emoji
|
||||
from .reddit.inline_media import InlineGif, InlineImage, InlineMedia, InlineVideo
|
||||
from .reddit.live import LiveThread, LiveUpdate
|
||||
from .reddit.message import Message, SubredditMessage
|
||||
from .reddit.modmail import ModmailAction, ModmailConversation, ModmailMessage
|
||||
from .reddit.more import MoreComments
|
||||
from .reddit.multi import Multireddit
|
||||
from .reddit.poll import PollData, PollOption
|
||||
from .reddit.redditor import Redditor
|
||||
from .reddit.removal_reasons import RemovalReason
|
||||
from .reddit.rules import Rule
|
||||
from .reddit.submission import Submission
|
||||
from .reddit.subreddit import Subreddit
|
||||
from .reddit.user_subreddit import UserSubreddit
|
||||
from .reddit.widgets import (
|
||||
Button,
|
||||
ButtonWidget,
|
||||
Calendar,
|
||||
CalendarConfiguration,
|
||||
CommunityList,
|
||||
CustomWidget,
|
||||
Hover,
|
||||
IDCard,
|
||||
Image,
|
||||
ImageData,
|
||||
ImageWidget,
|
||||
Menu,
|
||||
MenuLink,
|
||||
ModeratorsWidget,
|
||||
PostFlairWidget,
|
||||
RulesWidget,
|
||||
Styles,
|
||||
Submenu,
|
||||
SubredditWidgets,
|
||||
SubredditWidgetsModeration,
|
||||
TextArea,
|
||||
Widget,
|
||||
WidgetModeration,
|
||||
)
|
||||
from .reddit.wikipage import WikiPage
|
||||
from .redditors import Redditors
|
||||
from .stylesheet import Stylesheet
|
||||
from .subreddits import Subreddits
|
||||
from .trophy import Trophy
|
||||
from .user import User
|
||||
140
backend/venv/Lib/site-packages/praw/models/auth.py
Normal file
140
backend/venv/Lib/site-packages/praw/models/auth.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Provide the Auth class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from prawcore import Authorizer, ImplicitAuthorizer, UntrustedAuthenticator, session
|
||||
|
||||
from ..exceptions import InvalidImplicitAuth, MissingRequiredAttributeException
|
||||
from ..util import _deprecate_args
|
||||
from .base import PRAWBase
|
||||
|
||||
|
||||
class Auth(PRAWBase):
|
||||
"""Auth provides an interface to Reddit's authorization."""
|
||||
|
||||
@property
|
||||
def limits(self) -> dict[str, str | int | None]:
|
||||
"""Return a dictionary containing the rate limit info.
|
||||
|
||||
The keys are:
|
||||
|
||||
:remaining: The number of requests remaining to be made in the current rate
|
||||
limit window.
|
||||
:reset_timestamp: A unix timestamp providing an upper bound on when the rate
|
||||
limit counters will reset.
|
||||
:used: The number of requests made in the current rate limit window.
|
||||
|
||||
All values are initially ``None`` as these values are set in response to issued
|
||||
requests.
|
||||
|
||||
The ``reset_timestamp`` value is an upper bound as the real timestamp is
|
||||
computed on Reddit's end in preparation for sending the response. This value may
|
||||
change slightly within a given window due to slight changes in response times
|
||||
and rounding.
|
||||
|
||||
"""
|
||||
data = self._reddit._core._rate_limiter
|
||||
return {
|
||||
"remaining": data.remaining,
|
||||
"reset_timestamp": data.reset_timestamp,
|
||||
"used": data.used,
|
||||
}
|
||||
|
||||
def authorize(self, code: str) -> str | None:
|
||||
"""Complete the web authorization flow and return the refresh token.
|
||||
|
||||
:param code: The code obtained through the request to the redirect uri.
|
||||
|
||||
:returns: The obtained refresh token, if available, otherwise ``None``.
|
||||
|
||||
The session's active authorization will be updated upon success.
|
||||
|
||||
"""
|
||||
authenticator = self._reddit._read_only_core._authorizer._authenticator
|
||||
authorizer = Authorizer(authenticator)
|
||||
authorizer.authorize(code)
|
||||
authorized_session = session(
|
||||
authorizer=authorizer, window_size=self._reddit.config.window_size
|
||||
)
|
||||
self._reddit._core = self._reddit._authorized_core = authorized_session
|
||||
return authorizer.refresh_token
|
||||
|
||||
@_deprecate_args("access_token", "expires_in", "scope")
|
||||
def implicit(self, *, access_token: str, expires_in: int, scope: str):
|
||||
"""Set the active authorization to be an implicit authorization.
|
||||
|
||||
:param access_token: The access_token obtained from Reddit's callback.
|
||||
:param expires_in: The number of seconds the ``access_token`` is valid for. The
|
||||
origin of this value was returned from Reddit's callback. You may need to
|
||||
subtract an offset before passing in this number to account for a delay
|
||||
between when Reddit prepared the response, and when you make this function
|
||||
call.
|
||||
:param scope: A space-delimited string of Reddit OAuth2 scope names as returned
|
||||
from Reddit's callback.
|
||||
|
||||
:raises: :class:`.InvalidImplicitAuth` if :class:`.Reddit` was initialized for a
|
||||
non-installed application type.
|
||||
|
||||
"""
|
||||
authenticator = self._reddit._read_only_core._authorizer._authenticator
|
||||
if not isinstance(authenticator, UntrustedAuthenticator):
|
||||
raise InvalidImplicitAuth
|
||||
implicit_session = session(
|
||||
authorizer=ImplicitAuthorizer(
|
||||
authenticator, access_token, expires_in, scope
|
||||
),
|
||||
window_size=self._reddit.config.window_size,
|
||||
)
|
||||
self._reddit._core = self._reddit._authorized_core = implicit_session
|
||||
|
||||
def scopes(self) -> set[str]:
|
||||
"""Return a set of scopes included in the current authorization.
|
||||
|
||||
For read-only authorizations this should return ``{"*"}``.
|
||||
|
||||
"""
|
||||
authorizer = self._reddit._core._authorizer
|
||||
if not authorizer.is_valid():
|
||||
authorizer.refresh()
|
||||
return authorizer.scopes
|
||||
|
||||
@_deprecate_args("scopes", "state", "duration", "implicit")
|
||||
def url(
|
||||
self,
|
||||
*,
|
||||
duration: str = "permanent",
|
||||
implicit: bool = False,
|
||||
scopes: list[str],
|
||||
state: str,
|
||||
) -> str:
|
||||
"""Return the URL used out-of-band to grant access to your application.
|
||||
|
||||
:param duration: Either ``"permanent"`` or ``"temporary"`` (default:
|
||||
``"permanent"``). ``"temporary"`` authorizations generate access tokens that
|
||||
last only 1 hour. ``"permanent"`` authorizations additionally generate a
|
||||
refresh token that expires 1 year after the last use and can be used
|
||||
indefinitely to generate new hour-long access tokens. This value is ignored
|
||||
when ``implicit=True``.
|
||||
:param implicit: For **installed** applications, this value can be set to use
|
||||
the implicit, rather than the code flow. When ``True``, the ``duration``
|
||||
argument has no effect as only temporary tokens can be retrieved.
|
||||
:param scopes: A list of OAuth scopes to request authorization for.
|
||||
:param state: A string that will be reflected in the callback to
|
||||
``redirect_uri``. This value should be temporarily unique to the client for
|
||||
whom the URL was generated for.
|
||||
|
||||
"""
|
||||
authenticator = self._reddit._read_only_core._authorizer._authenticator
|
||||
if authenticator.redirect_uri is self._reddit.config.CONFIG_NOT_SET:
|
||||
msg = "redirect_uri must be provided"
|
||||
raise MissingRequiredAttributeException(msg)
|
||||
if isinstance(authenticator, UntrustedAuthenticator):
|
||||
return authenticator.authorize_url(
|
||||
"temporary" if implicit else duration,
|
||||
scopes,
|
||||
state,
|
||||
implicit=implicit,
|
||||
)
|
||||
if implicit:
|
||||
raise InvalidImplicitAuth
|
||||
return authenticator.authorize_url(duration, scopes, state)
|
||||
49
backend/venv/Lib/site-packages/praw/models/base.py
Normal file
49
backend/venv/Lib/site-packages/praw/models/base.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Provide the PRAWBase superclass."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class PRAWBase:
|
||||
"""Superclass for all models in PRAW."""
|
||||
|
||||
@staticmethod
|
||||
def _safely_add_arguments(
|
||||
*, arguments: dict[str, Any], key: str, **new_arguments: Any
|
||||
):
|
||||
"""Replace arguments[key] with a deepcopy and update.
|
||||
|
||||
This method is often called when new parameters need to be added to a request.
|
||||
By calling this method and adding the new or updated parameters we can insure we
|
||||
don't modify the dictionary passed in by the caller.
|
||||
|
||||
"""
|
||||
value = deepcopy(arguments[key]) if key in arguments else {}
|
||||
value.update(new_arguments)
|
||||
arguments[key] = value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, data: dict[str, Any], reddit: praw.Reddit) -> Any:
|
||||
"""Return an instance of ``cls`` from ``data``.
|
||||
|
||||
:param data: The structured data.
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
return cls(reddit, _data=data)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any] | None):
|
||||
"""Initialize a :class:`.PRAWBase` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
self._reddit = reddit
|
||||
if _data:
|
||||
for attribute, value in _data.items():
|
||||
setattr(self, attribute, value)
|
||||
209
backend/venv/Lib/site-packages/praw/models/comment_forest.py
Normal file
209
backend/venv/Lib/site-packages/praw/models/comment_forest.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Provide CommentForest for submission comments."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from heapq import heappop, heappush
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..exceptions import DuplicateReplaceException
|
||||
from ..util import _deprecate_args
|
||||
from .reddit.more import MoreComments
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class CommentForest:
|
||||
"""A forest of comments starts with multiple top-level comments.
|
||||
|
||||
Each of these comments can be a tree of replies.
|
||||
|
||||
"""
|
||||
|
||||
def __getitem__(self, index: int) -> praw.models.Comment:
|
||||
"""Return the comment at position ``index`` in the list.
|
||||
|
||||
This method is to be used like an array access, such as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
first_comment = submission.comments[0]
|
||||
|
||||
Alternatively, the presence of this method enables one to iterate over all top
|
||||
level comments, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in submission.comments:
|
||||
print(comment.body)
|
||||
|
||||
"""
|
||||
return self._comments[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of top-level comments in the forest."""
|
||||
return len(self._comments)
|
||||
|
||||
def _insert_comment(self, comment: praw.models.Comment):
|
||||
if comment.name in self._submission._comments_by_id:
|
||||
raise DuplicateReplaceException
|
||||
comment.submission = self._submission
|
||||
if isinstance(comment, MoreComments) or comment.is_root:
|
||||
self._comments.append(comment)
|
||||
else:
|
||||
assert comment.parent_id in self._submission._comments_by_id, (
|
||||
"PRAW Error occurred. Please file a bug report and include the code"
|
||||
" that caused the error."
|
||||
)
|
||||
parent = self._submission._comments_by_id[comment.parent_id]
|
||||
parent.replies._comments.append(comment)
|
||||
|
||||
def list( # noqa: A003
|
||||
self,
|
||||
) -> list[praw.models.Comment | praw.models.MoreComments]:
|
||||
"""Return a flattened list of all comments.
|
||||
|
||||
This list may contain :class:`.MoreComments` instances if :meth:`.replace_more`
|
||||
was not called first.
|
||||
|
||||
"""
|
||||
comments = []
|
||||
queue = list(self)
|
||||
while queue:
|
||||
comment = queue.pop(0)
|
||||
comments.append(comment)
|
||||
if not isinstance(comment, MoreComments):
|
||||
queue.extend(comment.replies)
|
||||
return comments
|
||||
|
||||
@staticmethod
|
||||
def _gather_more_comments(
|
||||
tree: list[praw.models.MoreComments],
|
||||
*,
|
||||
parent_tree: list[praw.models.MoreComments] | None = None,
|
||||
) -> list[MoreComments]:
|
||||
"""Return a list of :class:`.MoreComments` objects obtained from tree."""
|
||||
more_comments = []
|
||||
queue = [(None, x) for x in tree]
|
||||
while queue:
|
||||
parent, comment = queue.pop(0)
|
||||
if isinstance(comment, MoreComments):
|
||||
heappush(more_comments, comment)
|
||||
if parent:
|
||||
comment._remove_from = parent.replies._comments
|
||||
else:
|
||||
comment._remove_from = parent_tree or tree
|
||||
else:
|
||||
for item in comment.replies:
|
||||
queue.append((comment, item))
|
||||
return more_comments
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
submission: praw.models.Submission,
|
||||
comments: list[praw.models.Comment] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.CommentForest` instance.
|
||||
|
||||
:param submission: An instance of :class:`.Submission` that is the parent of the
|
||||
comments.
|
||||
:param comments: Initialize the forest with a list of comments (default:
|
||||
``None``).
|
||||
|
||||
"""
|
||||
self._comments = comments
|
||||
self._submission = submission
|
||||
|
||||
def _update(self, comments: list[praw.models.Comment]):
|
||||
self._comments = comments
|
||||
for comment in comments:
|
||||
comment.submission = self._submission
|
||||
|
||||
@_deprecate_args("limit", "threshold")
|
||||
def replace_more(
|
||||
self, *, limit: int | None = 32, threshold: int = 0
|
||||
) -> list[praw.models.MoreComments]:
|
||||
"""Update the comment forest by resolving instances of :class:`.MoreComments`.
|
||||
|
||||
:param limit: The maximum number of :class:`.MoreComments` instances to replace.
|
||||
Each replacement requires 1 API request. Set to ``None`` to have no limit,
|
||||
or to ``0`` to remove all :class:`.MoreComments` instances without
|
||||
additional requests (default: ``32``).
|
||||
:param threshold: The minimum number of children comments a
|
||||
:class:`.MoreComments` instance must have in order to be replaced.
|
||||
:class:`.MoreComments` instances that represent "continue this thread" links
|
||||
unfortunately appear to have 0 children (default: ``0``).
|
||||
|
||||
:returns: A list of :class:`.MoreComments` instances that were not replaced.
|
||||
|
||||
:raises: ``prawcore.TooManyRequests`` when used concurrently.
|
||||
|
||||
For example, to replace up to 32 :class:`.MoreComments` instances of a
|
||||
submission try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("3hahrw")
|
||||
submission.comments.replace_more()
|
||||
|
||||
Alternatively, to replace :class:`.MoreComments` instances within the replies of
|
||||
a single comment try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("d8r4im1")
|
||||
comment.refresh()
|
||||
comment.replies.replace_more()
|
||||
|
||||
.. note::
|
||||
|
||||
This method can take a long time as each replacement will discover at most
|
||||
100 new :class:`.Comment` instances. As a result, consider looping and
|
||||
handling exceptions until the method returns successfully. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
while True:
|
||||
try:
|
||||
submission.comments.replace_more()
|
||||
break
|
||||
except PossibleExceptions:
|
||||
print("Handling replace_more exception")
|
||||
sleep(1)
|
||||
|
||||
.. warning::
|
||||
|
||||
If this method is called, and the comments are refreshed, calling this
|
||||
method again will result in a :class:`.DuplicateReplaceException`.
|
||||
|
||||
"""
|
||||
remaining = limit
|
||||
more_comments = self._gather_more_comments(self._comments)
|
||||
skipped = []
|
||||
|
||||
# Fetch largest more_comments until reaching the limit or the threshold
|
||||
while more_comments:
|
||||
item = heappop(more_comments)
|
||||
if remaining is not None and remaining <= 0 or item.count < threshold:
|
||||
skipped.append(item)
|
||||
item._remove_from.remove(item)
|
||||
continue
|
||||
|
||||
new_comments = item.comments(update=False)
|
||||
if remaining is not None:
|
||||
remaining -= 1
|
||||
|
||||
# Add new MoreComment objects to the heap of more_comments
|
||||
for more in self._gather_more_comments(
|
||||
new_comments, parent_tree=self._comments
|
||||
):
|
||||
more.submission = self._submission
|
||||
heappush(more_comments, more)
|
||||
# Insert all items into the tree
|
||||
for comment in new_comments:
|
||||
self._insert_comment(comment)
|
||||
|
||||
# Remove from forest
|
||||
item._remove_from.remove(item)
|
||||
|
||||
return more_comments + skipped
|
||||
32
backend/venv/Lib/site-packages/praw/models/front.py
Normal file
32
backend/venv/Lib/site-packages/praw/models/front.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Provide the Front class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from .listing.generator import ListingGenerator
|
||||
from .listing.mixins import SubredditListingMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Front(SubredditListingMixin):
|
||||
"""Front is a Listing class that represents the front page."""
|
||||
|
||||
def __init__(self, reddit: praw.Reddit):
|
||||
"""Initialize a :class:`.Front` instance."""
|
||||
super().__init__(reddit, _data=None)
|
||||
self._path = "/"
|
||||
|
||||
def best(self, **generator_kwargs: str | int) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for best items.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "best"), **generator_kwargs
|
||||
)
|
||||
379
backend/venv/Lib/site-packages/praw/models/helpers.py
Normal file
379
backend/venv/Lib/site-packages/praw/models/helpers.py
Normal file
@@ -0,0 +1,379 @@
|
||||
"""Provide the helper classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
from ..const import API_PATH
|
||||
from ..util import _deprecate_args
|
||||
from .base import PRAWBase
|
||||
from .reddit.draft import Draft
|
||||
from .reddit.live import LiveThread
|
||||
from .reddit.multi import Multireddit, Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class DraftHelper(PRAWBase):
|
||||
r"""Provide a set of functions to interact with :class:`.Draft` instances.
|
||||
|
||||
.. note::
|
||||
|
||||
The methods provided by this class will only work on the currently authenticated
|
||||
user's :class:`.Draft`\ s.
|
||||
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self, draft_id: str | None = None
|
||||
) -> list[praw.models.Draft] | praw.models.Draft:
|
||||
"""Return a list of :class:`.Draft` instances.
|
||||
|
||||
:param draft_id: When provided, this returns a :class:`.Draft` instance
|
||||
(default: ``None``).
|
||||
|
||||
:returns: A :class:`.Draft` instance if ``draft_id`` is provided. Otherwise, a
|
||||
list of :class:`.Draft` objects.
|
||||
|
||||
.. note::
|
||||
|
||||
Drafts fetched using a specific draft ID are lazily loaded, so you might
|
||||
have to access an attribute to get all the expected attributes.
|
||||
|
||||
This method can be used to fetch a specific draft by ID, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft_id = "124862bc-e1e9-11eb-aa4f-e68667a77cbb"
|
||||
draft = reddit.drafts(draft_id)
|
||||
print(draft)
|
||||
|
||||
"""
|
||||
if draft_id is not None:
|
||||
return Draft(self._reddit, id=draft_id)
|
||||
return self._draft_list()
|
||||
|
||||
def _draft_list(self) -> list[praw.models.Draft]:
|
||||
"""Get a list of :class:`.Draft` instances.
|
||||
|
||||
:returns: A list of :class:`.Draft` instances.
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["drafts"], params={"md_body": True})
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
is_public_link: bool = False,
|
||||
nsfw: bool = False,
|
||||
original_content: bool = False,
|
||||
selftext: str | None = None,
|
||||
send_replies: bool = True,
|
||||
spoiler: bool = False,
|
||||
subreddit: (
|
||||
str | praw.models.Subreddit | praw.models.UserSubreddit | None
|
||||
) = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**draft_kwargs: Any,
|
||||
) -> praw.models.Draft:
|
||||
"""Create a new :class:`.Draft`.
|
||||
|
||||
:param flair_id: The flair template to select (default: ``None``).
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text (default: ``None``). ``flair_id`` is
|
||||
required when ``flair_text`` is provided.
|
||||
:param is_public_link: Whether to enable public viewing of the draft before it
|
||||
is submitted (default: ``False``).
|
||||
:param nsfw: Whether the draft should be marked NSFW (default: ``False``).
|
||||
:param original_content: Whether the submission should be marked as original
|
||||
content (default: ``False``).
|
||||
:param selftext: The Markdown formatted content for a text submission draft. Use
|
||||
``None`` to make a title-only submission draft (default: ``None``).
|
||||
``selftext`` can not be provided if ``url`` is provided.
|
||||
:param send_replies: When ``True``, messages will be sent to the submission
|
||||
author when comments are made to the submission (default: ``True``).
|
||||
:param spoiler: Whether the submission should be marked as a spoiler (default:
|
||||
``False``).
|
||||
:param subreddit: The subreddit to create the draft for. This accepts a
|
||||
subreddit display name, :class:`.Subreddit` object, or
|
||||
:class:`.UserSubreddit` object. If ``None``, the :class:`.UserSubreddit` of
|
||||
currently authenticated user will be used (default: ``None``).
|
||||
:param title: The title of the draft (default: ``None``).
|
||||
:param url: The URL for a ``link`` submission draft (default: ``None``). ``url``
|
||||
can not be provided if ``selftext`` is provided.
|
||||
|
||||
Additional keyword arguments can be provided to handle new parameters as Reddit
|
||||
introduces them.
|
||||
|
||||
:returns: The new :class:`.Draft` object.
|
||||
|
||||
"""
|
||||
if selftext and url:
|
||||
msg = "Exactly one of 'selftext' or 'url' must be provided."
|
||||
raise TypeError(msg)
|
||||
if isinstance(subreddit, str):
|
||||
subreddit = self._reddit.subreddit(subreddit)
|
||||
|
||||
data = Draft._prepare_data(
|
||||
flair_id=flair_id,
|
||||
flair_text=flair_text,
|
||||
is_public_link=is_public_link,
|
||||
nsfw=nsfw,
|
||||
original_content=original_content,
|
||||
selftext=selftext,
|
||||
send_replies=send_replies,
|
||||
spoiler=spoiler,
|
||||
subreddit=subreddit,
|
||||
title=title,
|
||||
url=url,
|
||||
**draft_kwargs,
|
||||
)
|
||||
return self._reddit.post(API_PATH["draft"], data=data)
|
||||
|
||||
|
||||
class LiveHelper(PRAWBase):
|
||||
r"""Provide a set of functions to interact with :class:`.LiveThread`\ s."""
|
||||
|
||||
def __call__(self, id: str) -> praw.models.LiveThread:
|
||||
"""Return a new lazy instance of :class:`.LiveThread`.
|
||||
|
||||
This method is intended to be used as:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
livethread = reddit.live("ukaeu1ik4sw5")
|
||||
|
||||
:param id: A live thread ID, e.g., ``ukaeu1ik4sw5``.
|
||||
|
||||
"""
|
||||
return LiveThread(self._reddit, id=id)
|
||||
|
||||
@_deprecate_args("title", "description", "nsfw", "resources")
|
||||
def create(
|
||||
self,
|
||||
title: str,
|
||||
*,
|
||||
description: str | None = None,
|
||||
nsfw: bool = False,
|
||||
resources: str = None,
|
||||
) -> praw.models.LiveThread:
|
||||
"""Create a new :class:`.LiveThread`.
|
||||
|
||||
:param title: The title of the new :class:`.LiveThread`.
|
||||
:param description: The new :class:`.LiveThread`'s description.
|
||||
:param nsfw: Indicate whether this thread is not safe for work (default:
|
||||
``False``).
|
||||
:param resources: Markdown formatted information that is useful for the
|
||||
:class:`.LiveThread`.
|
||||
|
||||
:returns: The new :class:`.LiveThread` object.
|
||||
|
||||
"""
|
||||
return self._reddit.post(
|
||||
API_PATH["livecreate"],
|
||||
data={
|
||||
"description": description,
|
||||
"nsfw": nsfw,
|
||||
"resources": resources,
|
||||
"title": title,
|
||||
},
|
||||
)
|
||||
|
||||
def info(self, ids: list[str]) -> Generator[praw.models.LiveThread, None, None]:
|
||||
"""Fetch information about each live thread in ``ids``.
|
||||
|
||||
:param ids: A list of IDs for a live thread.
|
||||
|
||||
:returns: A generator that yields :class:`.LiveThread` instances.
|
||||
|
||||
:raises: ``prawcore.ServerError`` if invalid live threads are requested.
|
||||
|
||||
Requests will be issued in batches for each 100 IDs.
|
||||
|
||||
.. note::
|
||||
|
||||
This method doesn't support IDs for live updates.
|
||||
|
||||
.. warning::
|
||||
|
||||
Unlike :meth:`.Reddit.info`, the output of this method may not reflect the
|
||||
order of input.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
ids = ["3rgnbke2rai6hen7ciytwcxadi", "sw7bubeycai6hey4ciytwamw3a", "t8jnufucss07"]
|
||||
for thread in reddit.live.info(ids):
|
||||
print(thread.title)
|
||||
|
||||
"""
|
||||
if not isinstance(ids, list):
|
||||
msg = "ids must be a list"
|
||||
raise TypeError(msg)
|
||||
|
||||
def generator():
|
||||
for position in range(0, len(ids), 100):
|
||||
ids_chunk = ids[position : position + 100]
|
||||
url = API_PATH["live_info"].format(ids=",".join(ids_chunk))
|
||||
params = {"limit": 100} # 25 is used if not specified
|
||||
yield from self._reddit.get(url, params=params)
|
||||
|
||||
return generator()
|
||||
|
||||
def now(self) -> praw.models.LiveThread | None:
|
||||
"""Get the currently featured live thread.
|
||||
|
||||
:returns: The :class:`.LiveThread` object, or ``None`` if there is no currently
|
||||
featured live thread.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live.now() # LiveThread object or None
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["live_now"])
|
||||
|
||||
|
||||
class MultiredditHelper(PRAWBase):
|
||||
"""Provide a set of functions to interact with multireddits."""
|
||||
|
||||
@_deprecate_args("redditor", "name")
|
||||
def __call__(
|
||||
self, *, name: str, redditor: str | praw.models.Redditor
|
||||
) -> praw.models.Multireddit:
|
||||
"""Return a lazy instance of :class:`.Multireddit`.
|
||||
|
||||
:param name: The name of the multireddit.
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance who owns the
|
||||
multireddit.
|
||||
|
||||
"""
|
||||
path = f"/user/{redditor}/m/{name}"
|
||||
return Multireddit(self._reddit, _data={"name": name, "path": path})
|
||||
|
||||
@_deprecate_args(
|
||||
"display_name",
|
||||
"subreddits",
|
||||
"description_md",
|
||||
"icon_name",
|
||||
"key_color",
|
||||
"visibility",
|
||||
"weighting_scheme",
|
||||
)
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
description_md: str | None = None,
|
||||
display_name: str,
|
||||
icon_name: str | None = None,
|
||||
key_color: str | None = None,
|
||||
subreddits: str | praw.models.Subreddit,
|
||||
visibility: str = "private",
|
||||
weighting_scheme: str = "classic",
|
||||
) -> praw.models.Multireddit:
|
||||
"""Create a new :class:`.Multireddit`.
|
||||
|
||||
:param display_name: The display name for the new multireddit.
|
||||
:param subreddits: Subreddits to add to the new multireddit. Can be a list of
|
||||
either :class:`.Subreddit` instances or subreddit display names.
|
||||
:param description_md: Description for the new multireddit, formatted in
|
||||
markdown.
|
||||
:param icon_name: Can be one of: ``"art and design"``, ``"ask"``, ``"books"``,
|
||||
``"business"``, ``"cars"``, ``"comics"``, ``"cute animals"``, ``"diy"``,
|
||||
``"entertainment"``, ``"food and drink"``, ``"funny"``, ``"games"``,
|
||||
``"grooming"``, ``"health"``, ``"life advice"``, ``"military"``, ``"models
|
||||
pinup"``, ``"music"``, ``"news"``, ``"philosophy"``, ``"pictures and
|
||||
gifs"``, ``"science"``, ``"shopping"``, ``"sports"``, ``"style"``,
|
||||
``"tech"``, ``"travel"``, ``"unusual stories"``, ``"video"``, or ``None``.
|
||||
:param key_color: RGB hex color code of the form ``"#FFFFFF"``.
|
||||
:param visibility: Can be one of: ``"hidden"``, ``"private"``, or ``"public"``
|
||||
(default: ``"private"``).
|
||||
:param weighting_scheme: Can be one of: ``"classic"`` or ``"fresh"`` (default:
|
||||
``"classic"``).
|
||||
|
||||
:returns: The new :class:`.Multireddit` object.
|
||||
|
||||
"""
|
||||
model = {
|
||||
"description_md": description_md,
|
||||
"display_name": display_name,
|
||||
"icon_name": icon_name,
|
||||
"key_color": key_color,
|
||||
"subreddits": [{"name": str(sub)} for sub in subreddits],
|
||||
"visibility": visibility,
|
||||
"weighting_scheme": weighting_scheme,
|
||||
}
|
||||
return self._reddit.post(
|
||||
API_PATH["multireddit_base"], data={"model": dumps(model)}
|
||||
)
|
||||
|
||||
|
||||
class SubredditHelper(PRAWBase):
|
||||
"""Provide a set of functions to interact with Subreddits."""
|
||||
|
||||
def __call__(self, display_name: str) -> praw.models.Subreddit:
|
||||
"""Return a lazy instance of :class:`.Subreddit`.
|
||||
|
||||
:param display_name: The name of the subreddit.
|
||||
|
||||
"""
|
||||
lower_name = display_name.lower()
|
||||
|
||||
if lower_name == "random":
|
||||
return self._reddit.random_subreddit()
|
||||
if lower_name == "randnsfw":
|
||||
return self._reddit.random_subreddit(nsfw=True)
|
||||
|
||||
return Subreddit(self._reddit, display_name=display_name)
|
||||
|
||||
@_deprecate_args("name", "title", "link_type", "subreddit_type", "wikimode")
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
link_type: str = "any",
|
||||
subreddit_type: str = "public",
|
||||
title: str | None = None,
|
||||
wikimode: str = "disabled",
|
||||
**other_settings: str | None,
|
||||
) -> praw.models.Subreddit:
|
||||
"""Create a new :class:`.Subreddit`.
|
||||
|
||||
:param name: The name for the new subreddit.
|
||||
:param link_type: The types of submissions users can make. One of ``"any"``,
|
||||
``"link"``, or ``"self"`` (default: ``"any"``).
|
||||
:param subreddit_type: One of ``"archived"``, ``"employees_only"``,
|
||||
``"gold_only"``, ``"gold_restricted"``, ``"private"``, ``"public"``, or
|
||||
``"restricted"`` (default: ``"public"``).
|
||||
:param title: The title of the subreddit. When ``None`` or ``""`` use the value
|
||||
of ``name``.
|
||||
:param wikimode: One of ``"anyone"``, ``"disabled"``, or ``"modonly"`` (default:
|
||||
``"disabled"``).
|
||||
|
||||
Any keyword parameters not provided, or set explicitly to ``None``, will take on
|
||||
a default value assigned by the Reddit server.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`~.SubredditModeration.update` for documentation of other available
|
||||
settings.
|
||||
|
||||
"""
|
||||
Subreddit._create_or_update(
|
||||
_reddit=self._reddit,
|
||||
link_type=link_type,
|
||||
name=name,
|
||||
subreddit_type=subreddit_type,
|
||||
title=title or name,
|
||||
wikimode=wikimode,
|
||||
**other_settings,
|
||||
)
|
||||
return self(name)
|
||||
342
backend/venv/Lib/site-packages/praw/models/inbox.py
Normal file
342
backend/venv/Lib/site-packages/praw/models/inbox.py
Normal file
@@ -0,0 +1,342 @@
|
||||
"""Provide the Front class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
|
||||
from ..const import API_PATH
|
||||
from ..util import _deprecate_args
|
||||
from .base import PRAWBase
|
||||
from .listing.generator import ListingGenerator
|
||||
from .util import stream_generator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Inbox(PRAWBase):
|
||||
"""Inbox is a Listing class that represents the inbox."""
|
||||
|
||||
def all( # noqa: A003
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Message | praw.models.Comment]:
|
||||
"""Return a :class:`.ListingGenerator` for all inbox comments and messages.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To output the type and ID of all items available via this listing do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.inbox.all(limit=None):
|
||||
print(repr(item))
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, API_PATH["inbox"], **generator_kwargs)
|
||||
|
||||
def collapse(self, items: list[praw.models.Message]):
|
||||
"""Mark an inbox message as collapsed.
|
||||
|
||||
:param items: A list containing instances of :class:`.Message`.
|
||||
|
||||
Requests are batched at 25 items (reddit limit).
|
||||
|
||||
For example, to collapse all unread Messages, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import Message
|
||||
|
||||
unread_messages = []
|
||||
for item in reddit.inbox.unread(limit=None):
|
||||
if isinstance(item, Message):
|
||||
unread_messages.append(item)
|
||||
reddit.inbox.collapse(unread_messages)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.Message.uncollapse`
|
||||
|
||||
"""
|
||||
while items:
|
||||
data = {"id": ",".join(x.fullname for x in items[:25])}
|
||||
self._reddit.post(API_PATH["collapse"], data=data)
|
||||
items = items[25:]
|
||||
|
||||
def comment_replies(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment]:
|
||||
"""Return a :class:`.ListingGenerator` for comment replies.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To output the author of one request worth of comment replies try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for reply in reddit.inbox.comment_replies():
|
||||
print(reply.author)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["comment_replies"], **generator_kwargs
|
||||
)
|
||||
|
||||
def mark_all_read(self):
|
||||
"""Mark all messages as read with just one API call.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.inbox.mark_all_read()
|
||||
|
||||
.. note::
|
||||
|
||||
This method returns after Reddit acknowledges your request, instead of after
|
||||
the request has been fulfilled.
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["read_all_messages"])
|
||||
|
||||
def mark_read(self, items: list[praw.models.Comment | praw.models.Message]):
|
||||
"""Mark Comments or Messages as read.
|
||||
|
||||
:param items: A list containing instances of :class:`.Comment` and/or
|
||||
:class:`.Message` to be marked as read relative to the authorized user's
|
||||
inbox.
|
||||
|
||||
Requests are batched at 25 items (reddit limit).
|
||||
|
||||
For example, to mark all unread Messages as read, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import Message
|
||||
|
||||
unread_messages = []
|
||||
for item in reddit.inbox.unread(limit=None):
|
||||
if isinstance(item, Message):
|
||||
unread_messages.append(item)
|
||||
reddit.inbox.mark_read(unread_messages)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`.Comment.mark_read`
|
||||
- :meth:`.Message.mark_read`
|
||||
|
||||
"""
|
||||
while items:
|
||||
data = {"id": ",".join(x.fullname for x in items[:25])}
|
||||
self._reddit.post(API_PATH["read_message"], data=data)
|
||||
items = items[25:]
|
||||
|
||||
def mark_unread(self, items: list[praw.models.Comment | praw.models.Message]):
|
||||
"""Unmark Comments or Messages as read.
|
||||
|
||||
:param items: A list containing instances of :class:`.Comment` and/or
|
||||
:class:`.Message` to be marked as unread relative to the authorized user's
|
||||
inbox.
|
||||
|
||||
Requests are batched at 25 items (Reddit limit).
|
||||
|
||||
For example, to mark the first 10 items as unread try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
to_unread = list(reddit.inbox.all(limit=10))
|
||||
reddit.inbox.mark_unread(to_unread)
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`.Comment.mark_unread`
|
||||
- :meth:`.Message.mark_unread`
|
||||
|
||||
"""
|
||||
while items:
|
||||
data = {"id": ",".join(x.fullname for x in items[:25])}
|
||||
self._reddit.post(API_PATH["unread_message"], data=data)
|
||||
items = items[25:]
|
||||
|
||||
def mentions(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment]:
|
||||
r"""Return a :class:`.ListingGenerator` for mentions.
|
||||
|
||||
A mention is :class:`.Comment` in which the authorized redditor is named in its
|
||||
body like u/spez.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to output the author and body of the first 25 mentions try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for mention in reddit.inbox.mentions(limit=25):
|
||||
print(f"{mention.author}\\n{mention.body}\\n")
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, API_PATH["mentions"], **generator_kwargs)
|
||||
|
||||
def message(self, message_id: str) -> praw.models.Message:
|
||||
"""Return a :class:`.Message` corresponding to ``message_id``.
|
||||
|
||||
:param message_id: The base36 ID of a message.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
message = reddit.inbox.message("7bnlgu")
|
||||
|
||||
"""
|
||||
listing = self._reddit.get(API_PATH["message"].format(id=message_id))
|
||||
messages = {
|
||||
message.fullname: message for message in [listing[0]] + listing[0].replies
|
||||
}
|
||||
for _fullname, message in messages.items():
|
||||
message.parent = messages.get(message.parent_id, None)
|
||||
return messages[f"t4_{message_id.lower()}"]
|
||||
|
||||
def messages(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Message]:
|
||||
"""Return a :class:`.ListingGenerator` for inbox messages.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to output the subject of the most recent 5 messages try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for message in reddit.inbox.messages(limit=5):
|
||||
print(message.subject)
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, API_PATH["messages"], **generator_kwargs)
|
||||
|
||||
def sent(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Message]:
|
||||
"""Return a :class:`.ListingGenerator` for sent messages.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to output the recipient of the most recent 15 messages try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for message in reddit.inbox.sent(limit=15):
|
||||
print(message.dest)
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, API_PATH["sent"], **generator_kwargs)
|
||||
|
||||
def stream(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Message]:
|
||||
"""Yield new inbox items as they become available.
|
||||
|
||||
Items are yielded oldest first. Up to 100 historical items will initially be
|
||||
returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new inbox items, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.inbox.stream():
|
||||
print(item)
|
||||
|
||||
"""
|
||||
return stream_generator(self.unread, **stream_options)
|
||||
|
||||
def submission_replies(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment]:
|
||||
"""Return a :class:`.ListingGenerator` for submission replies.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To output the author of one request worth of submission replies try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for reply in reddit.inbox.submission_replies():
|
||||
print(reply.author)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["submission_replies"], **generator_kwargs
|
||||
)
|
||||
|
||||
def uncollapse(self, items: list[praw.models.Message]):
|
||||
"""Mark an inbox message as uncollapsed.
|
||||
|
||||
:param items: A list containing instances of :class:`.Message`.
|
||||
|
||||
Requests are batched at 25 items (reddit limit).
|
||||
|
||||
For example, to uncollapse all unread Messages, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import Message
|
||||
|
||||
unread_messages = []
|
||||
for item in reddit.inbox.unread(limit=None):
|
||||
if isinstance(item, Message):
|
||||
unread_messages.append(item)
|
||||
reddit.inbox.uncollapse(unread_messages)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.Message.collapse`
|
||||
|
||||
"""
|
||||
while items:
|
||||
data = {"id": ",".join(x.fullname for x in items[:25])}
|
||||
self._reddit.post(API_PATH["uncollapse"], data=data)
|
||||
items = items[25:]
|
||||
|
||||
@_deprecate_args("mark_read")
|
||||
def unread(
|
||||
self,
|
||||
*,
|
||||
mark_read: bool = False,
|
||||
**generator_kwargs: str | int | dict[str, str],
|
||||
) -> Iterator[praw.models.Comment | praw.models.Message]:
|
||||
"""Return a :class:`.ListingGenerator` for unread comments and messages.
|
||||
|
||||
:param mark_read: Marks the inbox as read (default: ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
This only marks the inbox as read not the messages. Use
|
||||
:meth:`.Inbox.mark_read` to mark the messages.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to output the author of unread comments try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import Comment
|
||||
|
||||
for item in reddit.inbox.unread(limit=None):
|
||||
if isinstance(item, Comment):
|
||||
print(item.author)
|
||||
|
||||
"""
|
||||
self._safely_add_arguments(
|
||||
arguments=generator_kwargs, key="params", mark=mark_read
|
||||
)
|
||||
return ListingGenerator(self._reddit, API_PATH["unread"], **generator_kwargs)
|
||||
@@ -0,0 +1 @@
|
||||
"""Package providing models that act like a list."""
|
||||
52
backend/venv/Lib/site-packages/praw/models/list/base.py
Normal file
52
backend/venv/Lib/site-packages/praw/models/list/base.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Provide the BaseList class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
|
||||
from ..base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class BaseList(PRAWBase):
|
||||
"""An abstract class to coerce a list into a :class:`.PRAWBase`."""
|
||||
|
||||
CHILD_ATTRIBUTE = None
|
||||
|
||||
def __contains__(self, item: Any) -> bool:
|
||||
"""Test if item exists in the list."""
|
||||
return item in getattr(self, self.CHILD_ATTRIBUTE)
|
||||
|
||||
def __getitem__(self, index: int) -> Any:
|
||||
"""Return the item at position index in the list."""
|
||||
return getattr(self, self.CHILD_ATTRIBUTE)[index]
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.BaseList` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
if self.CHILD_ATTRIBUTE is None:
|
||||
msg = "BaseList must be extended."
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
child_list = getattr(self, self.CHILD_ATTRIBUTE)
|
||||
for index, item in enumerate(child_list):
|
||||
child_list[index] = reddit._objector.objectify(item)
|
||||
|
||||
def __iter__(self) -> Iterator[Any]:
|
||||
"""Return an iterator to the list."""
|
||||
return getattr(self, self.CHILD_ATTRIBUTE).__iter__()
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of items in the list."""
|
||||
return len(getattr(self, self.CHILD_ATTRIBUTE))
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the list."""
|
||||
return str(getattr(self, self.CHILD_ATTRIBUTE))
|
||||
9
backend/venv/Lib/site-packages/praw/models/list/draft.py
Normal file
9
backend/venv/Lib/site-packages/praw/models/list/draft.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Provide the DraftList class."""
|
||||
|
||||
from .base import BaseList
|
||||
|
||||
|
||||
class DraftList(BaseList):
|
||||
"""A list of :class:`.Draft` objects. Works just like a regular list."""
|
||||
|
||||
CHILD_ATTRIBUTE = "drafts"
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Provide the ModeratedList class."""
|
||||
|
||||
from .base import BaseList
|
||||
|
||||
|
||||
class ModeratedList(BaseList):
|
||||
"""A list of moderated :class:`.Subreddit` objects. Works just like a regular list."""
|
||||
|
||||
CHILD_ATTRIBUTE = "data"
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Provide the RedditorList class."""
|
||||
|
||||
from .base import BaseList
|
||||
|
||||
|
||||
class RedditorList(BaseList):
|
||||
"""A list of :class:`.Redditor` objects. Works just like a regular list."""
|
||||
|
||||
CHILD_ATTRIBUTE = "children"
|
||||
14
backend/venv/Lib/site-packages/praw/models/list/trophy.py
Normal file
14
backend/venv/Lib/site-packages/praw/models/list/trophy.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Provide the TrophyList class."""
|
||||
|
||||
from .base import BaseList
|
||||
|
||||
|
||||
class TrophyList(BaseList):
|
||||
"""A list of :class:`.Trophy` objects. Works just like a regular list.
|
||||
|
||||
This class is solely used to parse responses from Reddit, so end users should not
|
||||
use this class directly.
|
||||
|
||||
"""
|
||||
|
||||
CHILD_ATTRIBUTE = "trophies"
|
||||
@@ -0,0 +1 @@
|
||||
"""Package providing models and mixins pertaining to Reddit listings."""
|
||||
25
backend/venv/Lib/site-packages/praw/models/listing/domain.py
Normal file
25
backend/venv/Lib/site-packages/praw/models/listing/domain.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Provide the DomainListing class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ...const import API_PATH
|
||||
from .mixins import BaseListingMixin, RisingListingMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class DomainListing(BaseListingMixin, RisingListingMixin):
|
||||
"""Provide a set of functions to interact with domain listings."""
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, domain: str):
|
||||
"""Initialize a :class:`.DomainListing` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param domain: The domain for which to obtain listings.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=None)
|
||||
self._path = API_PATH["domain"].format(domain=domain)
|
||||
102
backend/venv/Lib/site-packages/praw/models/listing/generator.py
Normal file
102
backend/venv/Lib/site-packages/praw/models/listing/generator.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""Provide the ListingGenerator class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
|
||||
from ..base import PRAWBase
|
||||
from .listing import FlairListing, ModNoteListing
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class ListingGenerator(PRAWBase, Iterator):
|
||||
"""Instances of this class generate :class:`.RedditBase` instances.
|
||||
|
||||
.. warning::
|
||||
|
||||
This class should not be directly utilized. Instead, you will find a number of
|
||||
methods that return instances of the class here_.
|
||||
|
||||
.. _here: https://praw.readthedocs.io/en/latest/search.html?q=ListingGenerator
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
url: str,
|
||||
limit: int = 100,
|
||||
params: dict[str, str | int] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.ListingGenerator` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param url: A URL returning a Reddit listing.
|
||||
:param limit: The number of content entries to fetch. If ``limit`` is ``None``,
|
||||
then fetch as many entries as possible. Most of Reddit's listings contain a
|
||||
maximum of 1000 items, and are returned 100 at a time. This class will
|
||||
automatically issue all necessary requests (default: ``100``).
|
||||
:param params: A dictionary containing additional query string parameters to
|
||||
send with the request.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=None)
|
||||
self._exhausted = False
|
||||
self._listing = None
|
||||
self._list_index = None
|
||||
self.limit = limit
|
||||
self.params = deepcopy(params) if params else {}
|
||||
self.params["limit"] = limit or 1024
|
||||
self.url = url
|
||||
self.yielded = 0
|
||||
|
||||
def __iter__(self) -> Any:
|
||||
"""Permit :class:`.ListingGenerator` to operate as an iterator."""
|
||||
return self
|
||||
|
||||
def __next__(self) -> Any:
|
||||
"""Permit :class:`.ListingGenerator` to operate as a generator."""
|
||||
if self.limit is not None and self.yielded >= self.limit:
|
||||
raise StopIteration
|
||||
|
||||
if self._listing is None or self._list_index >= len(self._listing):
|
||||
self._next_batch()
|
||||
|
||||
self._list_index += 1
|
||||
self.yielded += 1
|
||||
return self._listing[self._list_index - 1]
|
||||
|
||||
def _extract_sublist(self, listing: dict[str, Any] | list[Any]):
|
||||
if isinstance(listing, list):
|
||||
return listing[1] # for submission duplicates
|
||||
if isinstance(listing, dict):
|
||||
classes = [FlairListing, ModNoteListing]
|
||||
|
||||
for listing_type in classes:
|
||||
if listing_type.CHILD_ATTRIBUTE in listing:
|
||||
return listing_type(self._reddit, listing)
|
||||
else: # noqa: PLW0120
|
||||
msg = "The generator returned a dictionary PRAW didn't recognize. File a bug report at PRAW."
|
||||
raise ValueError(msg)
|
||||
return listing
|
||||
|
||||
def _next_batch(self):
|
||||
if self._exhausted:
|
||||
raise StopIteration
|
||||
|
||||
self._listing = self._reddit.get(self.url, params=self.params)
|
||||
self._listing = self._extract_sublist(self._listing)
|
||||
self._list_index = 0
|
||||
|
||||
if not self._listing:
|
||||
raise StopIteration
|
||||
|
||||
if self._listing.after and self._listing.after != self.params.get(
|
||||
self._listing.AFTER_PARAM
|
||||
):
|
||||
self.params[self._listing.AFTER_PARAM] = self._listing.after
|
||||
else:
|
||||
self._exhausted = True
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Provide the Listing class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..base import PRAWBase
|
||||
|
||||
|
||||
class Listing(PRAWBase):
|
||||
"""A listing is a collection of :class:`.RedditBase` instances."""
|
||||
|
||||
AFTER_PARAM = "after"
|
||||
CHILD_ATTRIBUTE = "children"
|
||||
|
||||
def __getitem__(self, index: int) -> Any:
|
||||
"""Return the item at position index in the list."""
|
||||
return getattr(self, self.CHILD_ATTRIBUTE)[index]
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of items in the Listing."""
|
||||
return len(getattr(self, self.CHILD_ATTRIBUTE))
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify the ``CHILD_ATTRIBUTE`` attribute."""
|
||||
if attribute == self.CHILD_ATTRIBUTE:
|
||||
value = self._reddit._objector.objectify(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
|
||||
class FlairListing(Listing):
|
||||
"""Special Listing for handling flair lists."""
|
||||
|
||||
CHILD_ATTRIBUTE = "users"
|
||||
|
||||
@property
|
||||
def after(self) -> Any | None:
|
||||
"""Return the next attribute or ``None``."""
|
||||
return getattr(self, "next", None)
|
||||
|
||||
|
||||
class ModNoteListing(Listing):
|
||||
"""Special Listing for handling :class:`.ModNote` lists."""
|
||||
|
||||
AFTER_PARAM = "before"
|
||||
CHILD_ATTRIBUTE = "mod_notes"
|
||||
|
||||
@property
|
||||
def after(self) -> Any | None:
|
||||
"""Return the next attribute or None."""
|
||||
if not getattr(self, "has_next_page", True):
|
||||
return None
|
||||
return getattr(self, "end_cursor", None)
|
||||
|
||||
|
||||
class ModeratorListing(Listing):
|
||||
"""Special Listing for handling moderator lists."""
|
||||
|
||||
CHILD_ATTRIBUTE = "moderators"
|
||||
|
||||
|
||||
class ModmailConversationsListing(Listing):
|
||||
"""Special Listing for handling :class:`.ModmailConversation` lists."""
|
||||
|
||||
CHILD_ATTRIBUTE = "conversations"
|
||||
|
||||
@property
|
||||
def after(self) -> str | None:
|
||||
"""Return the next attribute or ``None``."""
|
||||
try:
|
||||
return self.conversations[-1].id
|
||||
except IndexError:
|
||||
return None
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Package providing models that pertain to listing mixins."""
|
||||
|
||||
from .base import BaseListingMixin
|
||||
from .redditor import RedditorListingMixin
|
||||
from .rising import RisingListingMixin
|
||||
from .submission import SubmissionListingMixin
|
||||
from .subreddit import SubredditListingMixin
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Provide the BaseListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ....util import _deprecate_args
|
||||
from ...base import PRAWBase
|
||||
from ..generator import ListingGenerator
|
||||
|
||||
|
||||
class BaseListingMixin(PRAWBase):
|
||||
"""Adds minimum set of methods that apply to all listing objects."""
|
||||
|
||||
VALID_TIME_FILTERS = {"all", "day", "hour", "month", "week", "year"}
|
||||
|
||||
@staticmethod
|
||||
def _validate_time_filter(time_filter: str):
|
||||
"""Validate ``time_filter``.
|
||||
|
||||
:raises: :py:class:`ValueError` if ``time_filter`` is not valid.
|
||||
|
||||
"""
|
||||
if time_filter not in BaseListingMixin.VALID_TIME_FILTERS:
|
||||
valid_time_filters = ", ".join(
|
||||
map("{!r}".format, BaseListingMixin.VALID_TIME_FILTERS)
|
||||
)
|
||||
msg = f"'time_filter' must be one of: {valid_time_filters}"
|
||||
raise ValueError(msg)
|
||||
|
||||
def _prepare(self, *, arguments: dict[str, Any], sort: str) -> str:
|
||||
"""Fix for :class:`.Redditor` methods that use a query param rather than subpath."""
|
||||
if self.__dict__.get("_listing_use_sort"):
|
||||
self._safely_add_arguments(arguments=arguments, key="params", sort=sort)
|
||||
return self._path
|
||||
return urljoin(self._path, sort)
|
||||
|
||||
@_deprecate_args("time_filter")
|
||||
def controversial(
|
||||
self,
|
||||
*,
|
||||
time_filter: str = "all",
|
||||
**generator_kwargs: str | int | dict[str, str],
|
||||
) -> Iterator[Any]:
|
||||
"""Return a :class:`.ListingGenerator` for controversial items.
|
||||
|
||||
:param time_filter: Can be one of: ``"all"``, ``"day"``, ``"hour"``,
|
||||
``"month"``, ``"week"``, or ``"year"`` (default: ``"all"``).
|
||||
|
||||
:raises: :py:class:`ValueError` if ``time_filter`` is invalid.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
This method can be used like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.domain("imgur.com").controversial(time_filter="week")
|
||||
reddit.multireddit(redditor="samuraisam", name="programming").controversial(
|
||||
time_filter="day"
|
||||
)
|
||||
reddit.redditor("spez").controversial(time_filter="month")
|
||||
reddit.redditor("spez").comments.controversial(time_filter="year")
|
||||
reddit.redditor("spez").submissions.controversial(time_filter="all")
|
||||
reddit.subreddit("all").controversial(time_filter="hour")
|
||||
|
||||
"""
|
||||
self._validate_time_filter(time_filter)
|
||||
self._safely_add_arguments(
|
||||
arguments=generator_kwargs, key="params", t=time_filter
|
||||
)
|
||||
url = self._prepare(arguments=generator_kwargs, sort="controversial")
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
|
||||
def hot(self, **generator_kwargs: str | int | dict[str, str]) -> Iterator[Any]:
|
||||
"""Return a :class:`.ListingGenerator` for hot items.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
This method can be used like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.domain("imgur.com").hot()
|
||||
reddit.multireddit(redditor="samuraisam", name="programming").hot()
|
||||
reddit.redditor("spez").hot()
|
||||
reddit.redditor("spez").comments.hot()
|
||||
reddit.redditor("spez").submissions.hot()
|
||||
reddit.subreddit("all").hot()
|
||||
|
||||
"""
|
||||
generator_kwargs.setdefault("params", {})
|
||||
url = self._prepare(arguments=generator_kwargs, sort="hot")
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
|
||||
def new(self, **generator_kwargs: str | int | dict[str, str]) -> Iterator[Any]:
|
||||
"""Return a :class:`.ListingGenerator` for new items.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
This method can be used like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.domain("imgur.com").new()
|
||||
reddit.multireddit(redditor="samuraisam", name="programming").new()
|
||||
reddit.redditor("spez").new()
|
||||
reddit.redditor("spez").comments.new()
|
||||
reddit.redditor("spez").submissions.new()
|
||||
reddit.subreddit("all").new()
|
||||
|
||||
"""
|
||||
generator_kwargs.setdefault("params", {})
|
||||
url = self._prepare(arguments=generator_kwargs, sort="new")
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
|
||||
@_deprecate_args("time_filter")
|
||||
def top(
|
||||
self,
|
||||
*,
|
||||
time_filter: str = "all",
|
||||
**generator_kwargs: str | int | dict[str, str],
|
||||
) -> Iterator[Any]:
|
||||
"""Return a :class:`.ListingGenerator` for top items.
|
||||
|
||||
:param time_filter: Can be one of: ``"all"``, ``"day"``, ``"hour"``,
|
||||
``"month"``, ``"week"``, or ``"year"`` (default: ``"all"``).
|
||||
|
||||
:raises: :py:class:`ValueError` if ``time_filter`` is invalid.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
This method can be used like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.domain("imgur.com").top(time_filter="week")
|
||||
reddit.multireddit(redditor="samuraisam", name="programming").top(time_filter="day")
|
||||
reddit.redditor("spez").top(time_filter="month")
|
||||
reddit.redditor("spez").comments.top(time_filter="year")
|
||||
reddit.redditor("spez").submissions.top(time_filter="all")
|
||||
reddit.subreddit("all").top(time_filter="hour")
|
||||
|
||||
"""
|
||||
self._validate_time_filter(time_filter)
|
||||
self._safely_add_arguments(
|
||||
arguments=generator_kwargs, key="params", t=time_filter
|
||||
)
|
||||
url = self._prepare(arguments=generator_kwargs, sort="top")
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Provide the GildedListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ...base import PRAWBase
|
||||
from ..generator import ListingGenerator
|
||||
|
||||
|
||||
class GildedListingMixin(PRAWBase):
|
||||
"""Mixes in the gilded method."""
|
||||
|
||||
def gilded(self, **generator_kwargs: str | int | dict[str, str]) -> Iterator[Any]:
|
||||
"""Return a :class:`.ListingGenerator` for gilded items.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get gilded items in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.subreddit("test").gilded():
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "gilded"), **generator_kwargs
|
||||
)
|
||||
@@ -0,0 +1,224 @@
|
||||
"""Provide the RedditorListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ....util.cache import cachedproperty
|
||||
from ..generator import ListingGenerator
|
||||
from .base import BaseListingMixin
|
||||
from .gilded import GildedListingMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class SubListing(BaseListingMixin):
|
||||
"""Helper class for generating :class:`.ListingGenerator` objects."""
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, base_path: str, subpath: str):
|
||||
"""Initialize a :class:`.SubListing` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param base_path: The path to the object up to this point.
|
||||
:param subpath: The additional path to this sublisting.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=None)
|
||||
self._listing_use_sort = True
|
||||
self._reddit = reddit
|
||||
self._path = urljoin(base_path, subpath)
|
||||
|
||||
|
||||
class RedditorListingMixin(BaseListingMixin, GildedListingMixin):
|
||||
"""Adds additional methods pertaining to :class:`.Redditor` instances."""
|
||||
|
||||
@cachedproperty
|
||||
def comments(self) -> SubListing:
|
||||
r"""Provide an instance of :class:`.SubListing` for comment access.
|
||||
|
||||
For example, to output the first line of all new comments by u/spez try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.redditor("spez").comments.new(limit=None):
|
||||
print(comment.body.split("\\n", 1)[0][:79])
|
||||
|
||||
"""
|
||||
return SubListing(self._reddit, self._path, "comments")
|
||||
|
||||
@cachedproperty
|
||||
def submissions(self) -> SubListing:
|
||||
"""Provide an instance of :class:`.SubListing` for submission access.
|
||||
|
||||
For example, to output the title's of top 100 of all time submissions for u/spez
|
||||
try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.redditor("spez").submissions.top(time_filter="all"):
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
return SubListing(self._reddit, self._path, "submitted")
|
||||
|
||||
def downvoted(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for items the user has downvoted.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Comment` or
|
||||
:class:`.Submission` objects the user has downvoted.
|
||||
|
||||
:raises: ``prawcore.Forbidden`` if the user is not authorized to access the
|
||||
list.
|
||||
|
||||
.. note::
|
||||
|
||||
Since this function returns a :class:`.ListingGenerator` the exception
|
||||
may not occur until sometime after this function has returned.
|
||||
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get all downvoted items of the authenticated user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.user.me().downvoted():
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "downvoted"), **generator_kwargs
|
||||
)
|
||||
|
||||
def gildings(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for items the user has gilded.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Comment` or
|
||||
:class:`.Submission` objects the user has gilded.
|
||||
|
||||
:raises: ``prawcore.Forbidden`` if the user is not authorized to access the
|
||||
list.
|
||||
|
||||
.. note::
|
||||
|
||||
Since this function returns a :class:`.ListingGenerator` the exception
|
||||
may not occur until sometime after this function has returned.
|
||||
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get all gilded items of the authenticated user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.user.me().gildings():
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "gilded/given"), **generator_kwargs
|
||||
)
|
||||
|
||||
def hidden(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for items the user has hidden.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Comment` or
|
||||
:class:`.Submission` objects the user has hid.
|
||||
|
||||
:raises: ``prawcore.Forbidden`` if the user is not authorized to access the
|
||||
list.
|
||||
|
||||
.. note::
|
||||
|
||||
Since this function returns a :class:`.ListingGenerator` the exception
|
||||
may not occur until sometime after this function has returned.
|
||||
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get all hidden items of the authenticated user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.user.me().hidden():
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "hidden"), **generator_kwargs
|
||||
)
|
||||
|
||||
def saved(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for items the user has saved.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Comment` or
|
||||
:class:`.Submission` objects the user has saved.
|
||||
|
||||
:raises: ``prawcore.Forbidden`` if the user is not authorized to access the
|
||||
list.
|
||||
|
||||
.. note::
|
||||
|
||||
Since this function returns a :class:`.ListingGenerator` the exception
|
||||
may not occur until sometime after this function has returned.
|
||||
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get all saved items of the authenticated user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.user.me().saved(limit=None):
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "saved"), **generator_kwargs
|
||||
)
|
||||
|
||||
def upvoted(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment | praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for items the user has upvoted.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Comment` or
|
||||
:class:`.Submission` objects the user has upvoted.
|
||||
|
||||
:raises: ``prawcore.Forbidden`` if the user is not authorized to access the
|
||||
list.
|
||||
|
||||
.. note::
|
||||
|
||||
Since this function returns a :class:`.ListingGenerator` the exception
|
||||
may not occur until sometime after this function has returned.
|
||||
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get all upvoted items of the authenticated user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.user.me().upvoted():
|
||||
print(item.id)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "upvoted"), **generator_kwargs
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Provide the RisingListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ...base import PRAWBase
|
||||
from ..generator import ListingGenerator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class RisingListingMixin(PRAWBase):
|
||||
"""Mixes in the rising methods."""
|
||||
|
||||
def random_rising(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for random rising submissions.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get random rising submissions for r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.subreddit("test").random_rising():
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "randomrising"), **generator_kwargs
|
||||
)
|
||||
|
||||
def rising(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for rising submissions.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
For example, to get rising submissions for r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.subreddit("test").rising():
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, urljoin(self._path, "rising"), **generator_kwargs
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Provide the SubmissionListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
|
||||
from ....const import API_PATH
|
||||
from ...base import PRAWBase
|
||||
from ..generator import ListingGenerator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class SubmissionListingMixin(PRAWBase):
|
||||
"""Adds additional methods pertaining to :class:`.Submission` instances."""
|
||||
|
||||
def duplicates(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for the submission's duplicates.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
|
||||
for duplicate in submission.duplicates():
|
||||
# process each duplicate
|
||||
...
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.upvote`
|
||||
|
||||
"""
|
||||
url = API_PATH["duplicates"].format(submission_id=self.id)
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Provide the SubredditListingMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from ....util.cache import cachedproperty
|
||||
from ...base import PRAWBase
|
||||
from ..generator import ListingGenerator
|
||||
from .base import BaseListingMixin
|
||||
from .gilded import GildedListingMixin
|
||||
from .rising import RisingListingMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class CommentHelper(PRAWBase):
|
||||
"""Provide a set of functions to interact with a :class:`.Subreddit`'s comments."""
|
||||
|
||||
@property
|
||||
def _path(self) -> str:
|
||||
return urljoin(self.subreddit._path, "comments/")
|
||||
|
||||
def __call__(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Comment]:
|
||||
"""Return a :class:`.ListingGenerator` for the :class:`.Subreddit`'s comments.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
This method should be used in a way similar to the example below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.subreddit("test").comments(limit=25):
|
||||
print(comment.author)
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, self._path, **generator_kwargs)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit | SubredditListingMixin):
|
||||
"""Initialize a :class:`.CommentHelper` instance."""
|
||||
super().__init__(subreddit._reddit, _data=None)
|
||||
self.subreddit = subreddit
|
||||
|
||||
|
||||
class SubredditListingMixin(BaseListingMixin, GildedListingMixin, RisingListingMixin):
|
||||
"""Adds additional methods pertaining to subreddit-like instances."""
|
||||
|
||||
@cachedproperty
|
||||
def comments(self) -> CommentHelper:
|
||||
"""Provide an instance of :class:`.CommentHelper`.
|
||||
|
||||
For example, to output the author of the 25 most recent comments of r/test
|
||||
execute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.subreddit("test").comments(limit=25):
|
||||
print(comment.author)
|
||||
|
||||
"""
|
||||
return CommentHelper(self)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any] | None):
|
||||
"""Initialize a :class:`.SubredditListingMixin` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=_data)
|
||||
23
backend/venv/Lib/site-packages/praw/models/mod_action.py
Normal file
23
backend/venv/Lib/site-packages/praw/models/mod_action.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Provide the ModAction class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ModAction(PRAWBase):
|
||||
"""Represent a moderator action."""
|
||||
|
||||
@property
|
||||
def mod(self) -> praw.models.Redditor:
|
||||
"""Return the :class:`.Redditor` who the action was issued by."""
|
||||
return self._reddit.redditor(self._mod)
|
||||
|
||||
@mod.setter
|
||||
def mod(self, value: str | praw.models.Redditor):
|
||||
self._mod = value
|
||||
72
backend/venv/Lib/site-packages/praw/models/mod_note.py
Normal file
72
backend/venv/Lib/site-packages/praw/models/mod_note.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Provide the ModNote class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..endpoints import API_PATH
|
||||
from .base import PRAWBase
|
||||
|
||||
|
||||
class ModNote(PRAWBase):
|
||||
"""Represent a moderator note.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ====================================================================
|
||||
Attribute Description
|
||||
=============== ====================================================================
|
||||
``action`` If this note represents a moderator action, this field indicates the
|
||||
type of action. For example, ``"banuser"`` if the action was banning
|
||||
a user.
|
||||
``created_at`` Time the moderator note was created, represented in `Unix Time`_.
|
||||
``description`` If this note represents a moderator action, this field indicates the
|
||||
description of the action. For example, if the action was banning
|
||||
the user, this is the ban reason.
|
||||
``details`` If this note represents a moderator action, this field indicates the
|
||||
details of the action. For example, if the action was banning the
|
||||
user, this is the duration of the ban.
|
||||
``id`` The ID of the moderator note.
|
||||
``label`` The label applied to the note, currently one of:
|
||||
``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``, ``"HELPFUL_USER"``,
|
||||
``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``, ``"SPAM_WARNING"``,
|
||||
``"SPAM_WATCH"``, or ``None``.
|
||||
``moderator`` The moderator who created the note.
|
||||
``note`` The text of the note.
|
||||
``reddit_id`` The fullname of the object this note is attributed to, or ``None``
|
||||
if not set. If this note represents a moderators action, this is the
|
||||
fullname of the object the action was performed on.
|
||||
``subreddit`` The subreddit this note belongs to.
|
||||
``type`` The type of note, currently one of: ``"APPROVAL"``, ``"BAN"``,
|
||||
``"CONTENT_CHANGE"``, ``"INVITE"``, ``"MUTE"``, ``"NOTE"``,
|
||||
``"REMOVAL"``, or ``"SPAM"``.
|
||||
``user`` The redditor the note is for.
|
||||
=============== ====================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other: ModNote) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.id == other.id
|
||||
if isinstance(other, str):
|
||||
return self.id == other
|
||||
return super().__eq__(other)
|
||||
|
||||
def delete(self):
|
||||
"""Delete this note.
|
||||
|
||||
For example, to delete the last note for u/spez from r/test, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for note in reddit.subreddit("test").mod.notes("spez"):
|
||||
note.delete()
|
||||
|
||||
"""
|
||||
params = {
|
||||
"user": str(self.user),
|
||||
"subreddit": str(self.subreddit),
|
||||
"note_id": self.id,
|
||||
}
|
||||
self._reddit.delete(API_PATH["mod_notes"], params=params)
|
||||
696
backend/venv/Lib/site-packages/praw/models/mod_notes.py
Normal file
696
backend/venv/Lib/site-packages/praw/models/mod_notes.py
Normal file
@@ -0,0 +1,696 @@
|
||||
"""Provides classes for interacting with moderator notes."""
|
||||
|
||||
from itertools import islice
|
||||
from typing import TYPE_CHECKING, Any, Generator, List, Optional, Tuple, Union
|
||||
|
||||
from ..const import API_PATH
|
||||
from .base import PRAWBase
|
||||
from .listing.generator import ListingGenerator
|
||||
from .reddit.comment import Comment
|
||||
from .reddit.redditor import Redditor
|
||||
from .reddit.submission import Submission
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
RedditorType = Union[Redditor, str]
|
||||
SubredditType = Union["praw.models.Subreddit", str]
|
||||
ThingType = Union[Comment, Submission]
|
||||
|
||||
|
||||
class BaseModNotes:
|
||||
"""Provides base methods to interact with moderator notes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: "praw.Reddit",
|
||||
):
|
||||
"""Initialize a :class:`.BaseModNotes` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
self._reddit = reddit
|
||||
|
||||
def _all_generator(
|
||||
self,
|
||||
redditor: RedditorType,
|
||||
subreddit: SubredditType,
|
||||
**generator_kwargs: Any,
|
||||
):
|
||||
PRAWBase._safely_add_arguments(
|
||||
arguments=generator_kwargs,
|
||||
key="params",
|
||||
subreddit=subreddit,
|
||||
user=redditor,
|
||||
)
|
||||
return ListingGenerator(self._reddit, API_PATH["mod_notes"], **generator_kwargs)
|
||||
|
||||
def _bulk_generator(
|
||||
self, redditors: List[RedditorType], subreddits: List[SubredditType]
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
subreddits_iter = iter(subreddits)
|
||||
redditors_iter = iter(redditors)
|
||||
while True:
|
||||
subreddits_chunk = list(islice(subreddits_iter, 500))
|
||||
users_chunk = list(islice(redditors_iter, 500))
|
||||
if not any([subreddits_chunk, users_chunk]):
|
||||
break
|
||||
params = {
|
||||
"subreddits": ",".join(map(str, subreddits_chunk)),
|
||||
"users": ",".join(map(str, users_chunk)),
|
||||
}
|
||||
response = self._reddit.get(API_PATH["mod_notes_bulk"], params=params)
|
||||
for note_dict in response["mod_notes"]:
|
||||
yield self._reddit._objector.objectify(note_dict)
|
||||
|
||||
def _ensure_attribute(self, error_message: str, **attributes: Any) -> Any:
|
||||
attribute, _value = attributes.popitem()
|
||||
value = _value or getattr(self, attribute, None)
|
||||
if value is None:
|
||||
raise TypeError(error_message)
|
||||
return value
|
||||
|
||||
def _notes(
|
||||
self,
|
||||
all_notes: bool,
|
||||
redditors: List[RedditorType],
|
||||
subreddits: List[SubredditType],
|
||||
**generator_kwargs: Any,
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
if all_notes:
|
||||
for subreddit in subreddits:
|
||||
for redditor in redditors:
|
||||
yield from self._all_generator(
|
||||
redditor, subreddit, **generator_kwargs
|
||||
)
|
||||
else:
|
||||
yield from self._bulk_generator(redditors, subreddits)
|
||||
|
||||
def create(
|
||||
self,
|
||||
*,
|
||||
label: Optional[str] = None,
|
||||
note: str,
|
||||
redditor: Optional[RedditorType] = None,
|
||||
subreddit: Optional[SubredditType] = None,
|
||||
thing: Optional[Union[Comment, Submission, str]] = None,
|
||||
**other_settings: Any,
|
||||
) -> "praw.models.ModNote":
|
||||
"""Create a :class:`.ModNote` for a redditor in the specified subreddit.
|
||||
|
||||
:param label: The label for the note. As of this writing, this can be one of the
|
||||
following: ``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``,
|
||||
``"HELPFUL_USER"``, ``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``,
|
||||
``"SPAM_WARNING"``, ``"SPAM_WATCH"``, or ``None`` (default: ``None``).
|
||||
:param note: The content of the note. As of this writing, this is limited to 250
|
||||
characters.
|
||||
:param redditor: The redditor to create the note for (default: ``None``).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is required if ``thing`` is not provided or this is not
|
||||
called from a :class:`.Redditor` instance (e.g.,
|
||||
``reddit.redditor.notes``).
|
||||
|
||||
:param subreddit: The subreddit associated with the note (default: ``None``).
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is required if ``thing`` is not provided or this is not
|
||||
called from a :class:`.Subreddit` instance (e.g.,
|
||||
``reddit.subreddit.mod``).
|
||||
|
||||
:param thing: Either the fullname of a comment/submission, a :class:`.Comment`,
|
||||
or a :class:`.Submission` to associate with the note.
|
||||
:param other_settings: Additional keyword arguments can be provided to handle
|
||||
new parameters as Reddit introduces them.
|
||||
|
||||
:returns: The new :class:`.ModNote` object.
|
||||
|
||||
For example, to create a note for u/spez in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.notes.create(
|
||||
label="HELPFUL_USER", note="Test note", redditor="spez"
|
||||
)
|
||||
# or
|
||||
reddit.redditor("spez").mod.notes.create(
|
||||
label="HELPFUL_USER", note="Test note", subreddit="test"
|
||||
)
|
||||
# or
|
||||
reddit.notes.create(
|
||||
label="HELPFUL_USER", note="Test note", redditor="spez", subreddit="test"
|
||||
)
|
||||
|
||||
"""
|
||||
reddit_id = None
|
||||
if thing:
|
||||
if isinstance(thing, str):
|
||||
reddit_id = thing
|
||||
# this is to minimize the number of requests
|
||||
if not (
|
||||
getattr(self, "redditor", redditor)
|
||||
and getattr(self, "subreddit", subreddit)
|
||||
):
|
||||
# only fetch if we are missing either redditor or subreddit
|
||||
thing = next(self._reddit.info(fullnames=[thing]))
|
||||
else:
|
||||
reddit_id = thing.fullname
|
||||
redditor = getattr(self, "redditor", redditor) or thing.author
|
||||
subreddit = getattr(self, "subreddit", subreddit) or thing.subreddit
|
||||
redditor = self._ensure_attribute(
|
||||
"Either the 'redditor' or 'thing' parameters must be provided or this"
|
||||
" method must be called from a Redditor instance (e.g., 'redditor.notes').",
|
||||
redditor=redditor,
|
||||
)
|
||||
subreddit = self._ensure_attribute(
|
||||
"Either the 'subreddit' or 'thing' parameters must be provided or this"
|
||||
" method must be called from a Subreddit instance (e.g.,"
|
||||
" 'subreddit.mod.notes').",
|
||||
subreddit=subreddit,
|
||||
)
|
||||
data = {
|
||||
"user": str(redditor),
|
||||
"subreddit": str(subreddit),
|
||||
"note": note,
|
||||
}
|
||||
if label:
|
||||
data["label"] = label
|
||||
if reddit_id:
|
||||
data["reddit_id"] = reddit_id
|
||||
data.update(other_settings)
|
||||
return self._reddit.post(API_PATH["mod_notes"], data=data)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
*,
|
||||
delete_all: bool = False,
|
||||
note_id: Optional[str] = None,
|
||||
redditor: Optional[RedditorType] = None,
|
||||
subreddit: Optional[SubredditType] = None,
|
||||
):
|
||||
"""Delete note(s) for a redditor.
|
||||
|
||||
:param delete_all: When ``True``, delete all notes for the specified redditor in
|
||||
the specified subreddit (default: ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
This will make a request for each note.
|
||||
|
||||
:param note_id: The ID of the note to delete. This parameter is ignored if
|
||||
``delete_all`` is ``True``.
|
||||
:param redditor: The redditor to delete the note(s) for (default: ``None``). Can
|
||||
be a :class:`.Redditor` instance or a redditor name.
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is required if this method is **not** called from a
|
||||
:class:`.Redditor` instance (e.g., ``redditor.notes``).
|
||||
|
||||
:param subreddit: The subreddit to delete the note(s) from (default: ``None``).
|
||||
Can be a :class:`.Subreddit` instance or a subreddit name.
|
||||
|
||||
.. note::
|
||||
|
||||
This parameter is required if this method is **not** called from a
|
||||
:class:`.Subreddit` instance (e.g., ``reddit.subreddit.mod``).
|
||||
|
||||
|
||||
For example, to delete a note with the ID
|
||||
``"ModNote_d324b280-5ecc-435d-8159-3e259e84e339"``, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.notes.delete(
|
||||
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", redditor="spez"
|
||||
)
|
||||
# or
|
||||
reddit.redditor("spez").notes.delete(
|
||||
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339", subreddit="test"
|
||||
)
|
||||
# or
|
||||
reddit.notes.delete(
|
||||
note_id="ModNote_d324b280-5ecc-435d-8159-3e259e84e339",
|
||||
subreddit="test",
|
||||
redditor="spez",
|
||||
)
|
||||
|
||||
To delete all notes for u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.notes.delete(delete_all=True, redditor="spez")
|
||||
# or
|
||||
reddit.redditor("spez").notes.delete(delete_all=True, subreddit="test")
|
||||
# or
|
||||
reddit.notes.delete(delete_all=True, subreddit="test", redditor="spez")
|
||||
|
||||
"""
|
||||
redditor = self._ensure_attribute(
|
||||
"Either the 'redditor' parameter must be provided or this method must be"
|
||||
" called from a Redditor instance (e.g., 'redditor.notes').",
|
||||
redditor=redditor,
|
||||
)
|
||||
subreddit = self._ensure_attribute(
|
||||
"Either the 'subreddit' parameter must be provided or this method must be"
|
||||
" called from a Subreddit instance (e.g., 'subreddit.mod.notes').",
|
||||
subreddit=subreddit,
|
||||
)
|
||||
if not delete_all and note_id is None:
|
||||
msg = "Either 'note_id' or 'delete_all' must be provided."
|
||||
raise TypeError(msg)
|
||||
if delete_all:
|
||||
for note in self._notes(True, [redditor], [subreddit]):
|
||||
note.delete()
|
||||
else:
|
||||
params = {
|
||||
"user": str(redditor),
|
||||
"subreddit": str(subreddit),
|
||||
"note_id": note_id,
|
||||
}
|
||||
self._reddit.delete(API_PATH["mod_notes"], params=params)
|
||||
|
||||
|
||||
class RedditorModNotes(BaseModNotes):
|
||||
"""Provides methods to interact with moderator notes at the redditor level.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the provided subreddit(s).
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
for note in redditor.notes.subreddits("test"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, reddit: "praw.Reddit", redditor: RedditorType):
|
||||
"""Initialize a :class:`.RedditorModNotes` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param redditor: An instance of :class:`.Redditor`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit)
|
||||
self.redditor = redditor
|
||||
|
||||
def subreddits(
|
||||
self,
|
||||
*subreddits: SubredditType,
|
||||
all_notes: Optional[bool] = None,
|
||||
**generator_kwargs: Any,
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
"""Return notes for this :class:`.Redditor` from one or more subreddits.
|
||||
|
||||
:param subreddits: One or more subreddits to retrieve the notes from. Must be
|
||||
either a :class:`.Subreddit` or a subreddit name.
|
||||
:param all_notes: Whether to return all notes or only the latest note (default:
|
||||
``True`` if only one subreddit is provided otherwise ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
Setting this to ``True`` will result in a request for each subreddit.
|
||||
|
||||
|
||||
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
|
||||
if this redditor doesn't have any notes) per subreddit in their relative
|
||||
order. If ``all_notes`` is ``True``, this will yield all notes or ``None``
|
||||
from each subreddit for this redditor.
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
for note in redditor.notes.subreddits("test"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, the latest note for u/spez from r/test and r/redditdev can be
|
||||
iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
subreddit = reddit.subreddit("redditdev")
|
||||
|
||||
for note in redditor.notes.subreddits("test", subreddit):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, **all** the notes for u/spez in r/test and r/redditdev can be
|
||||
iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
subreddit = reddit.subreddit("redditdev")
|
||||
|
||||
for note in redditor.notes.subreddits("test", subreddit, all_notes=True):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
if len(subreddits) == 0:
|
||||
msg = "At least 1 subreddit must be provided."
|
||||
raise ValueError(msg)
|
||||
if all_notes is None:
|
||||
all_notes = len(subreddits) == 1
|
||||
return self._notes(
|
||||
all_notes,
|
||||
[self.redditor] * len(subreddits),
|
||||
list(subreddits),
|
||||
**generator_kwargs,
|
||||
)
|
||||
|
||||
|
||||
class SubredditModNotes(BaseModNotes):
|
||||
"""Provides methods to interact with moderator notes at the subreddit level.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of this subreddit.
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
|
||||
for note in subreddit.mod.notes.redditors("spez"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, reddit: "praw.Reddit", subreddit: SubredditType):
|
||||
"""Initialize a :class:`.SubredditModNotes` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param subreddit: An instance of :class:`.Subreddit`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit)
|
||||
self.subreddit = subreddit
|
||||
|
||||
def redditors(
|
||||
self,
|
||||
*redditors: RedditorType,
|
||||
all_notes: Optional[bool] = None,
|
||||
**generator_kwargs: Any,
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
"""Return notes from this :class:`.Subreddit` for one or more redditors.
|
||||
|
||||
:param redditors: One or more redditors to retrieve notes for. Must be either a
|
||||
:class:`.Redditor` or a redditor name.
|
||||
:param all_notes: Whether to return all notes or only the latest note (default:
|
||||
``True`` if only one redditor is provided otherwise ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
Setting this to ``True`` will result in a request for each redditor.
|
||||
|
||||
|
||||
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
|
||||
if the user doesn't have any notes in this subreddit) per redditor in their
|
||||
relative order. If ``all_notes`` is ``True``, this will yield all notes for
|
||||
each redditor.
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
|
||||
for note in subreddit.mod.notes.redditors("spez"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, the latest note for u/spez and u/bboe from r/test can be iterated
|
||||
through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
redditor = reddit.redditor("bboe")
|
||||
|
||||
for note in subreddit.mod.notes.redditors("spez", redditor):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, **all** the notes for both u/spez and u/bboe in r/test can be
|
||||
iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
redditor = reddit.redditor("bboe")
|
||||
|
||||
for note in subreddit.mod.notes.redditors("spez", redditor, all_notes=True):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
if len(redditors) == 0:
|
||||
msg = "At least 1 redditor must be provided."
|
||||
raise ValueError(msg)
|
||||
if all_notes is None:
|
||||
all_notes = len(redditors) == 1
|
||||
return self._notes(
|
||||
all_notes,
|
||||
list(redditors),
|
||||
[self.subreddit] * len(redditors),
|
||||
**generator_kwargs,
|
||||
)
|
||||
|
||||
|
||||
class RedditModNotes(BaseModNotes):
|
||||
"""Provides methods to interact with moderator notes at a global level.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the provided subreddit(s).
|
||||
|
||||
For example, the latest note for u/spez in r/redditdev and r/test, and for u/bboe in
|
||||
r/redditdev can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("bboe")
|
||||
subreddit = reddit.subreddit("redditdev")
|
||||
|
||||
pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]
|
||||
|
||||
for note in reddit.notes(pairs=pairs):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
all_notes: bool = False,
|
||||
pairs: Optional[List[Tuple[SubredditType, RedditorType]]] = None,
|
||||
redditors: Optional[List[RedditorType]] = None,
|
||||
subreddits: Optional[List[SubredditType]] = None,
|
||||
things: Optional[List[ThingType]] = None,
|
||||
**generator_kwargs: Any,
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
"""Get note(s) for each subreddit/user pair, or ``None`` if they don't have any.
|
||||
|
||||
:param all_notes: Whether to return all notes or only the latest note for each
|
||||
subreddit/redditor pair (default: ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
Setting this to ``True`` will result in a request for each unique
|
||||
subreddit/redditor pair. If ``subreddits`` and ``redditors`` are
|
||||
provided, this will make a request equivalent to number of redditors
|
||||
multiplied by the number of subreddits.
|
||||
|
||||
:param pairs: A list of subreddit/redditor tuples.
|
||||
|
||||
.. note::
|
||||
|
||||
Required if ``subreddits``, ``redditors``, nor ``things`` are provided.
|
||||
|
||||
:param redditors: A list of redditors to return notes for. This parameter is
|
||||
used in tandem with ``subreddits`` to get notes from multiple subreddits for
|
||||
each of the provided redditors.
|
||||
|
||||
.. note::
|
||||
|
||||
Required if ``items`` or ``things`` is not provided or if ``subreddits``
|
||||
**is** provided.
|
||||
|
||||
:param subreddits: A list of subreddits to return notes for. This parameter is
|
||||
used in tandem with ``redditors`` to get notes for multiple redditors from
|
||||
each of the provided subreddits.
|
||||
|
||||
.. note::
|
||||
|
||||
Required if ``items`` or ``things`` is not provided or if ``redditors``
|
||||
**is** provided.
|
||||
|
||||
:param things: A list of comments and/or submissions to return notes for.
|
||||
:param generator_kwargs: Additional keyword arguments passed to the generator.
|
||||
This parameter is ignored when ``all_notes`` is ``False``.
|
||||
|
||||
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
|
||||
if the user doesn't have any notes) per entry in their relative order. If
|
||||
``all_notes`` is ``True``, this will yield all notes for each entry.
|
||||
|
||||
.. note::
|
||||
|
||||
This method will merge the subreddits and redditors provided from ``pairs``,
|
||||
``redditors``, ``subreddits``, and ``things``.
|
||||
|
||||
.. note::
|
||||
|
||||
This method accepts :class:`.Redditor` instances or redditor names and
|
||||
:class:`.Subreddit` instances or subreddit names where applicable.
|
||||
|
||||
For example, the latest note for u/spez in r/redditdev and r/test, and for
|
||||
u/bboe in r/redditdev can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("bboe")
|
||||
subreddit = reddit.subreddit("redditdev")
|
||||
|
||||
pairs = [(subreddit, "spez"), ("test", "spez"), (subreddit, redditor)]
|
||||
|
||||
for note in reddit.notes(pairs=pairs):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, **all** the notes for u/spez and u/bboe in r/announcements,
|
||||
r/redditdev, and r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("bboe")
|
||||
subreddit = reddit.subreddit("redditdev")
|
||||
|
||||
for note in reddit.notes(
|
||||
redditors=["spez", redditor],
|
||||
subreddits=["announcements", subreddit, "test"],
|
||||
all_notes=True,
|
||||
):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, the latest note for the authors of the last 5 comments and
|
||||
submissions from r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submissions = list(reddit.subreddit("test").new(limit=5))
|
||||
comments = list(reddit.subreddit("test").comments(limit=5))
|
||||
|
||||
for note in reddit.notes(things=submissions + comments):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
.. note::
|
||||
|
||||
Setting ``all_notes`` to ``True`` will make a request for each redditor and
|
||||
subreddit combination. The previous example will make 6 requests.
|
||||
|
||||
"""
|
||||
if pairs is None:
|
||||
pairs = []
|
||||
if redditors is None:
|
||||
redditors = []
|
||||
if subreddits is None:
|
||||
subreddits = []
|
||||
if things is None:
|
||||
things = []
|
||||
if not (pairs + redditors + subreddits + things):
|
||||
msg = "Either the 'pairs', 'redditors', 'subreddits', or 'things' parameters must be provided."
|
||||
raise TypeError(msg)
|
||||
if (
|
||||
len(redditors) * len(subreddits) == 0
|
||||
and len(redditors) + len(subreddits) > 0
|
||||
):
|
||||
raise TypeError(
|
||||
"'redditors' must be non-empty if 'subreddits' is not empty."
|
||||
if len(subreddits) > 0
|
||||
else "'subreddits' must be non-empty if 'redditors' is not empty."
|
||||
)
|
||||
|
||||
merged_redditors = []
|
||||
merged_subreddits = []
|
||||
items = (
|
||||
pairs
|
||||
+ [
|
||||
(subreddit, redditor)
|
||||
for redditor in set(redditors)
|
||||
for subreddit in set(subreddits)
|
||||
]
|
||||
+ things
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if isinstance(item, (Comment, Submission)):
|
||||
merged_redditors.append(item.author.name)
|
||||
merged_subreddits.append(item.subreddit.display_name)
|
||||
elif isinstance(item, Tuple):
|
||||
subreddit, redditor = item
|
||||
merged_redditors.append(redditor)
|
||||
merged_subreddits.append(subreddit)
|
||||
else:
|
||||
msg = f"Cannot get subreddit and author fields from type {type(item)}"
|
||||
raise ValueError(msg)
|
||||
return self._notes(
|
||||
all_notes, merged_redditors, merged_subreddits, **generator_kwargs
|
||||
)
|
||||
|
||||
def things(
|
||||
self,
|
||||
*things: ThingType,
|
||||
all_notes: Optional[bool] = None,
|
||||
**generator_kwargs: Any,
|
||||
) -> Generator["praw.models.ModNote", None, None]:
|
||||
"""Return notes associated with the author of a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param things: One or more things to return notes on. Must be a
|
||||
:class:`.Comment` or :class:`.Submission`.
|
||||
:param all_notes: Whether to return all notes, or only the latest (default:
|
||||
``True`` if only one thing is provided otherwise ``False``).
|
||||
|
||||
.. note::
|
||||
|
||||
Setting this to ``True`` will result in a request for each thing.
|
||||
|
||||
|
||||
:returns: A generator that yields the most recent :class:`.ModNote` (or ``None``
|
||||
if the user doesn't have any notes) per entry in their relative order. If
|
||||
``all_notes`` is ``True``, this will yield all notes for each entry.
|
||||
|
||||
For example, to get the latest note for the authors of the top 25 submissions in
|
||||
r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submissions = reddit.subreddit("test").top(limit=25)
|
||||
for note in reddit.notes.things(*submissions):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
For example, to get the latest note for the authors of the last 25 comments in
|
||||
r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comments = reddit.subreddit("test").comments(limit=25)
|
||||
for note in reddit.notes.things(*comments):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
subreddits = []
|
||||
redditors = []
|
||||
for thing in things:
|
||||
subreddits.append(thing.subreddit)
|
||||
redditors.append(thing.author)
|
||||
if all_notes is None:
|
||||
all_notes = len(things) == 1
|
||||
return self._notes(all_notes, redditors, subreddits, **generator_kwargs)
|
||||
208
backend/venv/Lib/site-packages/praw/models/preferences.py
Normal file
208
backend/venv/Lib/site-packages/praw/models/preferences.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Provide the Preferences class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..const import API_PATH
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class Preferences:
|
||||
"""A class for Reddit preferences.
|
||||
|
||||
The :class:`.Preferences` class provides access to the Reddit preferences of the
|
||||
currently authenticated user.
|
||||
|
||||
"""
|
||||
|
||||
def __call__(self) -> dict[str, bool | int | str]:
|
||||
"""Return the preference settings of the authenticated user as a dict.
|
||||
|
||||
This method is intended to be accessed as ``reddit.user.preferences()`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
preferences = reddit.user.preferences()
|
||||
print(preferences["show_link_flair"])
|
||||
|
||||
See https://www.reddit.com/dev/api#GET_api_v1_me_prefs for the list of possible
|
||||
values.
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["preferences"])
|
||||
|
||||
def __init__(self, reddit: praw.Reddit):
|
||||
"""Initialize a :class:`.Preferences` instance.
|
||||
|
||||
:param reddit: The :class:`.Reddit` instance.
|
||||
|
||||
"""
|
||||
self._reddit = reddit
|
||||
|
||||
def update(self, **preferences: bool | int | str) -> dict[str, bool | int | str]:
|
||||
"""Modify the specified settings.
|
||||
|
||||
:param accept_pms: Who can send you personal messages (one of ``"everyone"`` or
|
||||
``"whitelisted"``).
|
||||
:param activity_relevant_ads: Allow Reddit to use your activity on Reddit to
|
||||
show you more relevant advertisements.
|
||||
:param allow_clicktracking: Allow Reddit to log my outbound clicks for
|
||||
personalization.
|
||||
:param beta: I would like to beta test features for Reddit.
|
||||
:param clickgadget: Show me links I've recently viewed.
|
||||
:param collapse_read_messages: Collapse messages after I've read them.
|
||||
:param compress: Compress the link display.
|
||||
:param creddit_autorenew: Use a creddit to automatically renew my gold if it
|
||||
expires.
|
||||
:param default_comment_sort: Default comment sort (one of ``"confidence"``,
|
||||
``"top"``, ``"new"``, ``"controversial"``, ``"old"``, ``"random"``,
|
||||
``"qa"``, or ``"live"``).
|
||||
:param bool domain_details: Show additional details in the domain text when
|
||||
available, such as the source subreddit or the content author's url/name.
|
||||
:param bool email_chat_request: Send chat requests as emails.
|
||||
:param bool email_comment_reply: Send comment replies as emails.
|
||||
:param bool email_digests: Send email digests.
|
||||
:param bool email_messages: Send messages as emails.
|
||||
:param bool email_post_reply: Send post replies as emails.
|
||||
:param bool email_private_message: Send private messages as emails.
|
||||
:param bool email_unsubscribe_all: Unsubscribe from all emails.
|
||||
:param bool email_upvote_comment: Send comment upvote updates as emails.
|
||||
:param bool email_upvote_post: Send post upvote updates as emails.
|
||||
:param bool email_user_new_follower: Send new follower alerts as emails.
|
||||
:param bool email_username_mention: Send username mentions as emails.
|
||||
:param bool enable_default_themes: Use Reddit theme.
|
||||
:param bool feed_recommendations_enabled: Enable feed recommendations.
|
||||
:param str geopopular: Location (one of ``"GLOBAL"``, ``"AR"``, ``"AU"``,
|
||||
``"BG"``, ``"CA"``, ``"CL"``, ``"CO"``, ``"CZ"``, ``"FI"``, ``"GB"``,
|
||||
``"GR"``, ``"HR"``, ``"HU"``, ``"IE"``, ``"IN"``, ``"IS"``, ``"JP"``,
|
||||
``"MX"``, ``"MY"``, ``"NZ"``, ``"PH"``, ``"PL"``, ``"PR"``, ``"PT"``,
|
||||
``"RO"``, ``"RS"``, ``"SE"``, ``"SG"``, ``"TH"``, ``"TR"``, ``"TW"``,
|
||||
``"US"``, ``"US_AK"``, ``"US_AL"``, ``"US_AR"``, ``"US_AZ"``, ``"US_CA"``,
|
||||
``"US_CO"``, ``"US_CT"``, ``"US_DC"``, ``"US_DE"``, ``"US_FL"``,
|
||||
``"US_GA"``, ``"US_HI"``, ``"US_IA"``, ``"US_ID"``, ``"US_IL"``,
|
||||
``"US_IN"``, ``"US_KS"``, ``"US_KY"``, ``"US_LA"``, ``"US_MA"``,
|
||||
``"US_MD"``, ``"US_ME"``, ``"US_MI"``, ``"US_MN"``, ``"US_MO"``,
|
||||
``"US_MS"``, ``"US_MT"``, ``"US_NC"``, ``"US_ND"``, ``"US_NE"``,
|
||||
``"US_NH"``, ``"US_NJ"``, ``"US_NM"``, ``"US_NV"``, ``"US_NY"``,
|
||||
``"US_OH"``, ``"US_OK"``, ``"US_OR"``, ``"US_PA"``, ``"US_RI"``,
|
||||
``"US_SC"``, ``"US_SD"``, ``"US_TN"``, ``"US_TX"``, ``"US_UT"``,
|
||||
``"US_VA"``, ``"US_VT"``, ``"US_WA"``, ``"US_WI"``, ``"US_WV"``, or
|
||||
``"US_WY"``).
|
||||
:param bool hide_ads: Hide ads.
|
||||
:param bool hide_downs: Don't show me submissions after I've downvoted them,
|
||||
except my own.
|
||||
:param bool hide_from_robots: Don't allow search engines to index my user
|
||||
profile.
|
||||
:param bool hide_ups: Don't show me submissions after I've upvoted them, except
|
||||
my own.
|
||||
:param bool highlight_controversial: Show a dagger on comments voted
|
||||
controversial.
|
||||
:param bool highlight_new_comments: Highlight new comments.
|
||||
:param bool ignore_suggested_sort: Ignore suggested sorts.
|
||||
:param bool in_redesign_beta: In redesign beta.
|
||||
:param bool label_nsfw: Label posts that are not safe for work.
|
||||
:param str lang: Interface language (IETF language tag, underscore separated).
|
||||
:param bool legacy_search: Show legacy search page.
|
||||
:param bool live_orangereds: Send message notifications in my browser.
|
||||
:param bool mark_messages_read: Mark messages as read when I open my inbox.
|
||||
:param str media: Thumbnail preference (one of ``"on"``, ``"off"``, or
|
||||
``"subreddit"``).
|
||||
:param str media_preview: Media preview preference (one of ``"on"``, ``"off"``,
|
||||
or ``"subreddit"``).
|
||||
:param int min_comment_score: Don't show me comments with a score less than this
|
||||
number (between ``-100`` and ``100``).
|
||||
:param int min_link_score: Don't show me submissions with a score less than this
|
||||
number (between ``-100`` and ``100``).
|
||||
:param bool monitor_mentions: Notify me when people say my username.
|
||||
:param bool newwindow: Open links in a new window.
|
||||
:param bool nightmode: Enable night mode.
|
||||
:param bool no_profanity: Don't show thumbnails or media previews for anything
|
||||
labeled NSFW.
|
||||
:param int num_comments: Display this many comments by default (between ``1``
|
||||
and ``500``).
|
||||
:param int numsites: Number of links to display at once (between ``1`` and
|
||||
``100``).
|
||||
:param bool organic: Show the spotlight box on the home feed.
|
||||
:param str other_theme: Subreddit theme to use (subreddit name).
|
||||
:param bool over_18: I am over eighteen years old and willing to view adult
|
||||
content.
|
||||
:param bool private_feeds: Enable private RSS feeds.
|
||||
:param bool profile_opt_out: View user profiles on desktop using legacy mode.
|
||||
:param bool public_votes: Make my votes public.
|
||||
:param bool research: Allow my data to be used for research purposes.
|
||||
:param bool search_include_over_18: Include not safe for work (NSFW) search
|
||||
results in searches.
|
||||
:param bool send_crosspost_messages: Send crosspost messages.
|
||||
:param bool send_welcome_messages: Send welcome messages.
|
||||
:param bool show_flair: Show user flair.
|
||||
:param bool show_gold_expiration: Show how much gold you have remaining on your
|
||||
userpage.
|
||||
:param bool show_link_flair: Show link flair.
|
||||
:param bool show_location_based_recommendations: Show location based
|
||||
recommendations.
|
||||
:param bool show_presence: Show presence.
|
||||
:param bool show_promote: Show promote.
|
||||
:param bool show_stylesheets: Allow subreddits to show me custom themes.
|
||||
:param bool show_trending: Show trending subreddits on the home feed.
|
||||
:param bool show_twitter: Show a link to your Twitter account on your profile.
|
||||
:param bool store_visits: Store visits.
|
||||
:param bool theme_selector: Theme selector (subreddit name).
|
||||
:param bool third_party_data_personalized_ads: Allow Reddit to use data provided
|
||||
by third-parties to show you more relevant advertisements on Reddit.
|
||||
:param bool third_party_personalized_ads: Allow personalization of
|
||||
advertisements.
|
||||
:param bool third_party_site_data_personalized_ads: Allow personalization of
|
||||
advertisements using third-party website data.
|
||||
:param bool third_party_site_data_personalized_content: Allow personalization of
|
||||
content using third-party website data.
|
||||
:param bool threaded_messages: Show message conversations in the inbox.
|
||||
:param bool threaded_modmail: Enable threaded modmail display.
|
||||
:param bool top_karma_subreddits: Top karma subreddits.
|
||||
:param bool use_global_defaults: Use global defaults.
|
||||
:param bool video_autoplay: Autoplay Reddit videos on the desktop comments page.
|
||||
|
||||
Additional keyword arguments can be provided to handle new settings as Reddit
|
||||
introduces them.
|
||||
|
||||
See https://www.reddit.com/dev/api#PATCH_api_v1_me_prefs for the most up-to-date
|
||||
list of possible parameters.
|
||||
|
||||
This is intended to be used like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.user.preferences.update(show_link_flair=True)
|
||||
|
||||
This method returns the new state of the preferences as a ``dict``, which can be
|
||||
used to check whether a change went through.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
original_preferences = reddit.user.preferences()
|
||||
new_preferences = reddit.user.preferences.update(invalid_param=123)
|
||||
print(original_preferences == new_preferences) # True, no change
|
||||
|
||||
.. warning::
|
||||
|
||||
Passing an unknown parameter name or an illegal value (such as an int when a
|
||||
boolean is expected) does not result in an error from the Reddit API. As a
|
||||
consequence, any invalid input will fail silently. To verify that changes
|
||||
have been made, use the return value of this method, which is a dict of the
|
||||
preferences after the update action has been performed.
|
||||
|
||||
Some preferences have names that are not valid keyword arguments in Python. To
|
||||
update these, construct a ``dict`` and use ``**`` to unpack it as keyword
|
||||
arguments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.user.preferences.update(**{"third_party_data_personalized_ads": False})
|
||||
|
||||
"""
|
||||
return self._reddit.patch(
|
||||
API_PATH["preferences"], data={"json": dumps(preferences)}
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Provide all models that map to Reddit objects."""
|
||||
95
backend/venv/Lib/site-packages/praw/models/reddit/base.py
Normal file
95
backend/venv/Lib/site-packages/praw/models/reddit/base.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""Provide the RedditBase class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ...endpoints import API_PATH
|
||||
from ...exceptions import InvalidURL
|
||||
from ..base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class RedditBase(PRAWBase):
|
||||
"""Base class that represents actual Reddit objects."""
|
||||
|
||||
@staticmethod
|
||||
def _url_parts(url: str) -> list[str]:
|
||||
parsed = urlparse(url)
|
||||
if not parsed.netloc:
|
||||
raise InvalidURL(url)
|
||||
return parsed.path.rstrip("/").split("/")
|
||||
|
||||
def __eq__(self, other: Any | str) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other.lower() == str(self).lower()
|
||||
return (
|
||||
isinstance(other, self.__class__)
|
||||
and str(self).lower() == str(other).lower()
|
||||
)
|
||||
|
||||
def __getattr__(self, attribute: str) -> Any:
|
||||
"""Return the value of ``attribute``."""
|
||||
if not attribute.startswith("_") and not self._fetched:
|
||||
self._fetch()
|
||||
return getattr(self, attribute)
|
||||
msg = f"{self.__class__.__name__!r} object has no attribute {attribute!r}"
|
||||
raise AttributeError(msg)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self).lower())
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
_data: dict[str, Any] | None,
|
||||
_extra_attribute_to_check: str | None = None,
|
||||
_fetched: bool = False,
|
||||
_str_field: bool = True,
|
||||
):
|
||||
"""Initialize a :class:`.RedditBase` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._fetched = _fetched
|
||||
if _str_field and self.STR_FIELD not in self.__dict__:
|
||||
if (
|
||||
_extra_attribute_to_check is not None
|
||||
and _extra_attribute_to_check in self.__dict__
|
||||
):
|
||||
return
|
||||
msg = f"An invalid value was specified for {self.STR_FIELD}. Check that the argument for the {self.STR_FIELD} parameter is not empty."
|
||||
raise ValueError(msg)
|
||||
|
||||
def __ne__(self, other: object) -> bool:
|
||||
"""Return whether the other instance differs from the current."""
|
||||
return not self == other
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"{self.__class__.__name__}({self.STR_FIELD}={str(self)!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the instance."""
|
||||
return getattr(self, self.STR_FIELD)
|
||||
|
||||
def _fetch(self):
|
||||
self._fetched = True
|
||||
|
||||
def _fetch_data(self):
|
||||
name, fields, params = self._fetch_info()
|
||||
path = API_PATH[name].format(**fields)
|
||||
return self._reddit.request(method="GET", params=params, path=path)
|
||||
|
||||
def _reset_attributes(self, *attributes: str):
|
||||
for attribute in attributes:
|
||||
if attribute in self.__dict__:
|
||||
del self.__dict__[attribute]
|
||||
self._fetched = False
|
||||
586
backend/venv/Lib/site-packages/praw/models/reddit/collections.py
Normal file
586
backend/venv/Lib/site-packages/praw/models/reddit/collections.py
Normal file
@@ -0,0 +1,586 @@
|
||||
"""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
|
||||
355
backend/venv/Lib/site-packages/praw/models/reddit/comment.py
Normal file
355
backend/venv/Lib/site-packages/praw/models/reddit/comment.py
Normal file
@@ -0,0 +1,355 @@
|
||||
"""Provide the Comment class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException, InvalidURL
|
||||
from ...util.cache import cachedproperty
|
||||
from ..comment_forest import CommentForest
|
||||
from .base import RedditBase
|
||||
from .mixins import (
|
||||
FullnameMixin,
|
||||
InboxableMixin,
|
||||
ThingModerationMixin,
|
||||
UserContentMixin,
|
||||
)
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Comment(InboxableMixin, UserContentMixin, FullnameMixin, RedditBase):
|
||||
"""A class that represents a Reddit comment.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
================= =================================================================
|
||||
Attribute Description
|
||||
================= =================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the comment, as Markdown.
|
||||
``body_html`` The body of the comment, as HTML.
|
||||
``created_utc`` Time the comment was created, represented in `Unix Time`_.
|
||||
``distinguished`` Whether or not the comment is distinguished.
|
||||
``edited`` Whether or not the comment has been edited.
|
||||
``id`` The ID of the comment.
|
||||
``is_submitter`` Whether or not the comment author is also the author of the
|
||||
submission.
|
||||
``link_id`` The submission ID that the comment belongs to.
|
||||
``parent_id`` The ID of the parent comment (prefixed with ``t1_``). If it is a
|
||||
top-level comment, this returns the submission ID instead
|
||||
(prefixed with ``t3_``).
|
||||
``permalink`` A permalink for the comment. :class:`.Comment` objects from the
|
||||
inbox have a ``context`` attribute instead.
|
||||
``replies`` Provides an instance of :class:`.CommentForest`.
|
||||
``saved`` Whether or not the comment is saved.
|
||||
``score`` The number of upvotes for the comment.
|
||||
``stickied`` Whether or not the comment is stickied.
|
||||
``submission`` Provides an instance of :class:`.Submission`. The submission that
|
||||
the comment belongs to.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit`. The subreddit that
|
||||
the comment belongs to.
|
||||
``subreddit_id`` The subreddit ID that the comment belongs to.
|
||||
================= =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
MISSING_COMMENT_MESSAGE = "This comment does not appear to be in the comment tree"
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def id_from_url(url: str) -> str:
|
||||
"""Get the ID of a comment from the full URL."""
|
||||
parts = RedditBase._url_parts(url)
|
||||
try:
|
||||
comment_index = parts.index("comments")
|
||||
except ValueError:
|
||||
raise InvalidURL(url) from None
|
||||
|
||||
if len(parts) - 4 != comment_index:
|
||||
raise InvalidURL(url)
|
||||
return parts[-1]
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.comment.CommentModeration:
|
||||
"""Provide an instance of :class:`.CommentModeration`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
|
||||
"""
|
||||
return CommentModeration(self)
|
||||
|
||||
@property
|
||||
def _kind(self):
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["comment"]
|
||||
|
||||
@property
|
||||
def is_root(self) -> bool:
|
||||
"""Return ``True`` when the comment is a top-level comment."""
|
||||
parent_type = self.parent_id.split("_", 1)[0]
|
||||
return parent_type == self._reddit.config.kinds["submission"]
|
||||
|
||||
@property
|
||||
def replies(self) -> CommentForest:
|
||||
"""Provide an instance of :class:`.CommentForest`.
|
||||
|
||||
This property may return an empty list if the comment has not been refreshed
|
||||
with :meth:`.refresh`
|
||||
|
||||
Sort order and reply limit can be set with the ``reply_sort`` and
|
||||
``reply_limit`` attributes before replies are fetched, including any call to
|
||||
:meth:`.refresh`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment.reply_sort = "new"
|
||||
comment.refresh()
|
||||
replies = comment.replies
|
||||
|
||||
.. note::
|
||||
|
||||
The appropriate values for ``reply_sort`` include ``"confidence"``,
|
||||
``"controversial"``, ``"new"``, ``"old"``, ``"q&a"``, and ``"top"``.
|
||||
|
||||
"""
|
||||
if isinstance(self._replies, list):
|
||||
self._replies = CommentForest(self.submission, self._replies)
|
||||
return self._replies
|
||||
|
||||
@property
|
||||
def submission(self) -> praw.models.Submission:
|
||||
"""Return the :class:`.Submission` object this comment belongs to."""
|
||||
if not self._submission: # Comment not from submission
|
||||
self._submission = self._reddit.submission(self._extract_submission_id())
|
||||
return self._submission
|
||||
|
||||
@submission.setter
|
||||
def submission(self, submission: praw.models.Submission):
|
||||
"""Update the :class:`.Submission` associated with the :class:`.Comment`."""
|
||||
submission._comments_by_id[self.fullname] = self
|
||||
self._submission = submission
|
||||
for reply in getattr(self, "replies", []):
|
||||
reply.submission = submission
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
url: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Comment` instance."""
|
||||
if (id, url, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'id', 'url', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
fetched = False
|
||||
self._replies = []
|
||||
self._submission = None
|
||||
if id:
|
||||
self.id = id
|
||||
elif url:
|
||||
self.id = self.id_from_url(url)
|
||||
else:
|
||||
fetched = True
|
||||
super().__init__(reddit, _data=_data, _fetched=fetched)
|
||||
|
||||
def __setattr__(
|
||||
self,
|
||||
attribute: str,
|
||||
value: str | Redditor | CommentForest | praw.models.Subreddit,
|
||||
):
|
||||
"""Objectify author, replies, and subreddit."""
|
||||
if attribute == "author":
|
||||
value = Redditor.from_data(self._reddit, value)
|
||||
elif attribute == "replies":
|
||||
if value == "":
|
||||
value = []
|
||||
else:
|
||||
value = self._reddit._objector.objectify(value).children
|
||||
attribute = "_replies"
|
||||
elif attribute == "subreddit":
|
||||
value = self._reddit.subreddit(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _extract_submission_id(self):
|
||||
if "context" in self.__dict__:
|
||||
return self.context.rsplit("/", 4)[1]
|
||||
return self.link_id.split("_", 1)[1]
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
|
||||
if not data["children"]:
|
||||
msg = f"No data returned for comment {self.fullname}"
|
||||
raise ClientException(msg)
|
||||
|
||||
comment_data = data["children"][0]["data"]
|
||||
other = type(self)(self._reddit, _data=comment_data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "info", {}, {"id": self.fullname}
|
||||
|
||||
def parent(
|
||||
self,
|
||||
) -> Comment | praw.models.Submission:
|
||||
"""Return the parent of the comment.
|
||||
|
||||
The returned parent will be an instance of either :class:`.Comment`, or
|
||||
:class:`.Submission`.
|
||||
|
||||
If this comment was obtained through a :class:`.Submission`, then its entire
|
||||
ancestry should be immediately available, requiring no extra network requests.
|
||||
However, if this comment was obtained through other means, e.g.,
|
||||
``reddit.comment("COMMENT_ID")``, or ``reddit.inbox.comment_replies``, then the
|
||||
returned parent may be a lazy instance of either :class:`.Comment`, or
|
||||
:class:`.Submission`.
|
||||
|
||||
Lazy comment example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("cklhv0f")
|
||||
parent = comment.parent()
|
||||
# 'replies' is empty until the comment is refreshed
|
||||
print(parent.replies) # Output: []
|
||||
parent.refresh()
|
||||
print(parent.replies) # Output is at least: [Comment(id="cklhv0f")]
|
||||
|
||||
.. warning::
|
||||
|
||||
Successive calls to :meth:`.parent` may result in a network request per call
|
||||
when the comment is not obtained through a :class:`.Submission`. See below
|
||||
for an example of how to minimize requests.
|
||||
|
||||
If you have a deeply nested comment and wish to most efficiently discover its
|
||||
top-most :class:`.Comment` ancestor you can chain successive calls to
|
||||
:meth:`.parent` with calls to :meth:`.refresh` at every 9 levels. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
ancestor = comment
|
||||
refresh_counter = 0
|
||||
while not ancestor.is_root:
|
||||
ancestor = ancestor.parent()
|
||||
if refresh_counter % 9 == 0:
|
||||
ancestor.refresh()
|
||||
refresh_counter += 1
|
||||
print(f"Top-most Ancestor: {ancestor}")
|
||||
|
||||
The above code should result in 5 network requests to Reddit. Without the calls
|
||||
to :meth:`.refresh` it would make at least 31 network requests.
|
||||
|
||||
"""
|
||||
if self.parent_id == self.submission.fullname:
|
||||
return self.submission
|
||||
|
||||
if self.parent_id in self.submission._comments_by_id:
|
||||
# The Comment already exists, so simply return it
|
||||
return self.submission._comments_by_id[self.parent_id]
|
||||
|
||||
parent = Comment(self._reddit, self.parent_id.split("_", 1)[1])
|
||||
parent._submission = self.submission
|
||||
return parent
|
||||
|
||||
def refresh(self) -> Comment:
|
||||
"""Refresh the comment's attributes.
|
||||
|
||||
If using :meth:`.Reddit.comment` this method must be called in order to obtain
|
||||
the comment's replies.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.refresh()
|
||||
|
||||
"""
|
||||
if "context" in self.__dict__: # Using hasattr triggers a fetch
|
||||
comment_path = self.context.split("?", 1)[0]
|
||||
else:
|
||||
path = API_PATH["submission"].format(id=self.submission.id)
|
||||
comment_path = f"{path}_/{self.id}"
|
||||
|
||||
# The context limit appears to be 8, but let's ask for more anyway.
|
||||
params = {"context": 100}
|
||||
if "reply_limit" in self.__dict__:
|
||||
params["limit"] = self.reply_limit
|
||||
if "reply_sort" in self.__dict__:
|
||||
params["sort"] = self.reply_sort
|
||||
comment_list = self._reddit.get(comment_path, params=params)[1].children
|
||||
if not comment_list:
|
||||
raise ClientException(self.MISSING_COMMENT_MESSAGE)
|
||||
|
||||
# With context, the comment may be nested so we have to find it
|
||||
comment = None
|
||||
queue = comment_list[:]
|
||||
while queue and (comment is None or comment.id != self.id):
|
||||
comment = queue.pop()
|
||||
if isinstance(comment, Comment):
|
||||
queue.extend(comment._replies)
|
||||
|
||||
if comment.id != self.id:
|
||||
raise ClientException(self.MISSING_COMMENT_MESSAGE)
|
||||
|
||||
if self._submission is not None:
|
||||
del comment.__dict__["_submission"] # Don't replace if set
|
||||
self.__dict__.update(comment.__dict__)
|
||||
|
||||
for reply in comment_list:
|
||||
reply.submission = self.submission
|
||||
return self
|
||||
|
||||
|
||||
class CommentModeration(ThingModerationMixin):
|
||||
"""Provide a set of functions pertaining to :class:`.Comment` moderation.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
|
||||
"""
|
||||
|
||||
REMOVAL_MESSAGE_API = "removal_comment_message"
|
||||
|
||||
def __init__(self, comment: praw.models.Comment):
|
||||
"""Initialize a :class:`.CommentModeration` instance.
|
||||
|
||||
:param comment: The comment to moderate.
|
||||
|
||||
"""
|
||||
self.thing = comment
|
||||
|
||||
def show(self):
|
||||
"""Uncollapse a :class:`.Comment` that has been collapsed by Crowd Control.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Uncollapse a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.show()
|
||||
|
||||
"""
|
||||
url = API_PATH["show_comment"]
|
||||
|
||||
self.thing._reddit.post(url, data={"id": self.thing.fullname})
|
||||
308
backend/venv/Lib/site-packages/praw/models/reddit/draft.py
Normal file
308
backend/venv/Lib/site-packages/praw/models/reddit/draft.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""Provide the draft class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from .base import RedditBase
|
||||
from .subreddit import Subreddit
|
||||
from .user_subreddit import UserSubreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Draft(RedditBase):
|
||||
"""A class that represents a Reddit submission draft.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================== ======================================================
|
||||
Attribute Description
|
||||
========================== ======================================================
|
||||
``link_flair_template_id`` The link flair's ID.
|
||||
``link_flair_text`` The link flair's text content, or ``None`` if not
|
||||
flaired.
|
||||
``modified`` Time the submission draft was modified, represented in
|
||||
`Unix Time`_.
|
||||
``original_content`` Whether the submission draft will be set as original
|
||||
content.
|
||||
``selftext`` The submission draft's selftext. ``None`` if a link
|
||||
submission draft.
|
||||
``spoiler`` Whether the submission will be marked as a spoiler.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit` or
|
||||
:class:`.UserSubreddit` (if set).
|
||||
``title`` The title of the submission draft.
|
||||
``url`` The URL the submission draft links to.
|
||||
========================== ======================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@classmethod
|
||||
def _prepare_data(
|
||||
cls,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
is_public_link: bool | None = None,
|
||||
nsfw: bool | None = None,
|
||||
original_content: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
send_replies: bool | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: praw.models.Subreddit | praw.models.UserSubreddit | None = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**draft_kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
data = {
|
||||
"body": selftext or url,
|
||||
"flair_id": flair_id,
|
||||
"flair_text": flair_text,
|
||||
"is_public_link": is_public_link,
|
||||
"kind": "markdown" if selftext is not None else "link",
|
||||
"nsfw": nsfw,
|
||||
"original_content": original_content,
|
||||
"send_replies": send_replies,
|
||||
"spoiler": spoiler,
|
||||
"title": title,
|
||||
}
|
||||
if subreddit:
|
||||
data.update(
|
||||
{
|
||||
"subreddit": subreddit.fullname,
|
||||
"target": (
|
||||
"profile"
|
||||
if subreddit.display_name.startswith("u_")
|
||||
else "subreddit"
|
||||
),
|
||||
}
|
||||
)
|
||||
data.update(draft_kwargs)
|
||||
return data
|
||||
|
||||
def __init__(
|
||||
self, reddit: praw.Reddit, id: str | None = None, _data: dict[str, Any] = None
|
||||
):
|
||||
"""Initialize a :class:`.Draft` instance."""
|
||||
if (id, _data).count(None) != 1:
|
||||
msg = "Exactly one of 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
fetched = False
|
||||
if id:
|
||||
self.id = id
|
||||
elif len(_data) > 1:
|
||||
if _data["kind"] == "markdown":
|
||||
_data["selftext"] = _data.pop("body")
|
||||
elif _data["kind"] == "link":
|
||||
_data["url"] = _data.pop("body")
|
||||
fetched = True
|
||||
super().__init__(reddit, _data=_data, _fetched=fetched)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
if self._fetched:
|
||||
subreddit = (
|
||||
f" subreddit={self.subreddit.display_name!r}" if self.subreddit else ""
|
||||
)
|
||||
title = f" title={self.title!r}" if self.title else ""
|
||||
return f"{self.__class__.__name__}(id={self.id!r}{subreddit}{title})"
|
||||
return f"{self.__class__.__name__}(id={self.id!r})"
|
||||
|
||||
def _fetch(self):
|
||||
for draft in self._reddit.drafts():
|
||||
if draft.id == self.id:
|
||||
self.__dict__.update(draft.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = (
|
||||
f"The currently authenticated user not have a draft with an ID of {self.id}"
|
||||
)
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the :class:`.Draft`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("124862bc-e1e9-11eb-aa4f-e68667a77cbb")
|
||||
draft.delete()
|
||||
|
||||
"""
|
||||
self._reddit.delete(API_PATH["draft"], params={"draft_id": self.id})
|
||||
|
||||
def submit(
|
||||
self,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
nsfw: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: (
|
||||
str | praw.models.Subreddit | praw.models.UserSubreddit | None
|
||||
) = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**submit_kwargs: Any,
|
||||
) -> praw.models.Submission:
|
||||
"""Submit a draft.
|
||||
|
||||
:param flair_id: The flair template to select (default: ``None``).
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text (default: ``None``). ``flair_id`` is
|
||||
required when ``flair_text`` is provided.
|
||||
:param nsfw: Whether or not the submission should be marked NSFW (default:
|
||||
``None``).
|
||||
:param selftext: The Markdown formatted content for a ``text`` submission. Use
|
||||
an empty string, ``""``, to make a title-only submission (default:
|
||||
``None``).
|
||||
:param spoiler: Whether or not the submission should be marked as a spoiler
|
||||
(default: ``None``).
|
||||
:param subreddit: The subreddit to submit the draft to. This accepts a subreddit
|
||||
display name, :class:`.Subreddit` object, or :class:`.UserSubreddit` object.
|
||||
:param title: The title of the submission (default: ``None``).
|
||||
:param url: The URL for a ``link`` submission (default: ``None``).
|
||||
|
||||
:returns: A :class:`.Submission` object for the newly created submission.
|
||||
|
||||
.. note::
|
||||
|
||||
Parameters set here will override their respective :class:`.Draft`
|
||||
attributes.
|
||||
|
||||
Additional keyword arguments are passed to the :meth:`.Subreddit.submit` method.
|
||||
|
||||
For example, to submit a draft as is:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
submission = draft.submit()
|
||||
|
||||
For example, to submit a draft but use a different title than what is set:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
submission = draft.submit(title="New Title")
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`~.Subreddit.submit` to submit url posts and selftexts
|
||||
- :meth:`~.Subreddit.submit_gallery`. to submit more than one image in the
|
||||
same post
|
||||
- :meth:`~.Subreddit.submit_image` to submit images
|
||||
- :meth:`~.Subreddit.submit_poll` to submit polls
|
||||
- :meth:`~.Subreddit.submit_video` to submit videos and videogifs
|
||||
|
||||
"""
|
||||
submit_kwargs["draft_id"] = self.id
|
||||
if not (self.subreddit or subreddit):
|
||||
msg = "'subreddit' must be set on the Draft instance or passed as a keyword argument."
|
||||
raise ValueError(msg)
|
||||
for key, attribute in [
|
||||
("flair_id", flair_id),
|
||||
("flair_text", flair_text),
|
||||
("nsfw", nsfw),
|
||||
("selftext", selftext),
|
||||
("spoiler", spoiler),
|
||||
("title", title),
|
||||
("url", url),
|
||||
]:
|
||||
value = attribute or getattr(self, key, None)
|
||||
if value is not None:
|
||||
submit_kwargs[key] = value
|
||||
if isinstance(subreddit, str):
|
||||
_subreddit = self._reddit.subreddit(subreddit)
|
||||
elif isinstance(subreddit, (Subreddit, UserSubreddit)):
|
||||
_subreddit = subreddit
|
||||
else:
|
||||
_subreddit = self.subreddit
|
||||
return _subreddit.submit(**submit_kwargs)
|
||||
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
is_public_link: bool | None = None,
|
||||
nsfw: bool | None = None,
|
||||
original_content: bool | None = None,
|
||||
selftext: str | None = None,
|
||||
send_replies: bool | None = None,
|
||||
spoiler: bool | None = None,
|
||||
subreddit: (
|
||||
str | praw.models.Subreddit | praw.models.UserSubreddit | None
|
||||
) = None,
|
||||
title: str | None = None,
|
||||
url: str | None = None,
|
||||
**draft_kwargs: Any,
|
||||
):
|
||||
"""Update the :class:`.Draft`.
|
||||
|
||||
.. note::
|
||||
|
||||
Only provided values will be updated.
|
||||
|
||||
:param flair_id: The flair template to select.
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text. ``flair_id`` is required when
|
||||
``flair_text`` is provided.
|
||||
:param is_public_link: Whether to enable public viewing of the draft before it
|
||||
is submitted.
|
||||
:param nsfw: Whether the draft should be marked NSFW.
|
||||
:param original_content: Whether the submission should be marked as original
|
||||
content.
|
||||
:param selftext: The Markdown formatted content for a text submission draft. Use
|
||||
``None`` to make a title-only submission draft. ``selftext`` can not be
|
||||
provided if ``url`` is provided.
|
||||
:param send_replies: When ``True``, messages will be sent to the submission
|
||||
author when comments are made to the submission.
|
||||
:param spoiler: Whether the submission should be marked as a spoiler.
|
||||
:param subreddit: The subreddit to create the draft for. This accepts a
|
||||
subreddit display name, :class:`.Subreddit` object, or
|
||||
:class:`.UserSubreddit` object.
|
||||
:param title: The title of the draft.
|
||||
:param url: The URL for a ``link`` submission draft. ``url`` can not be provided
|
||||
if ``selftext`` is provided.
|
||||
|
||||
Additional keyword arguments can be provided to handle new parameters as Reddit
|
||||
introduces them.
|
||||
|
||||
For example, to update the title of a draft do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
draft = reddit.drafts("5f87d55c-e4fb-11eb-8965-6aeb41b0880e")
|
||||
draft.update(title="New title")
|
||||
|
||||
"""
|
||||
if isinstance(subreddit, str):
|
||||
subreddit = self._reddit.subreddit(subreddit)
|
||||
data = self._prepare_data(
|
||||
flair_id=flair_id,
|
||||
flair_text=flair_text,
|
||||
is_public_link=is_public_link,
|
||||
nsfw=nsfw,
|
||||
original_content=original_content,
|
||||
selftext=selftext,
|
||||
send_replies=send_replies,
|
||||
spoiler=spoiler,
|
||||
subreddit=subreddit,
|
||||
title=title,
|
||||
url=url,
|
||||
**draft_kwargs,
|
||||
)
|
||||
data["id"] = self.id
|
||||
_new_draft = self._reddit.put(API_PATH["draft"], data=data)
|
||||
_new_draft._fetch()
|
||||
self.__dict__.update(_new_draft.__dict__)
|
||||
246
backend/venv/Lib/site-packages/praw/models/reddit/emoji.py
Normal file
246
backend/venv/Lib/site-packages/praw/models/reddit/emoji.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""Provide the Emoji class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class Emoji(RedditBase):
|
||||
"""An individual :class:`.Emoji` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
====================== =================================================
|
||||
Attribute Description
|
||||
====================== =================================================
|
||||
``mod_flair_only`` Whether the emoji is restricted for mod use only.
|
||||
``name`` The name of the emoji.
|
||||
``post_flair_allowed`` Whether the emoji may appear in post flair.
|
||||
``url`` The URL of the emoji image.
|
||||
``user_flair_allowed`` Whether the emoji may appear in user flair.
|
||||
====================== =================================================
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "name"
|
||||
|
||||
def __eq__(self, other: str | Emoji) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
if isinstance(other, self.__class__):
|
||||
return str(self) == str(other) and other.subreddit == self.subreddit
|
||||
return super().__eq__(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self)) ^ hash(self.subreddit)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
name: str,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize an :class:`.Emoji` instance."""
|
||||
self.name = name
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for emoji in self.subreddit.emoji:
|
||||
if emoji.name == self.name:
|
||||
self.__dict__.update(emoji.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"r/{self.subreddit} does not have the emoji {self.name}"
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete an emoji from this subreddit by :class:`.Emoji`.
|
||||
|
||||
To delete ``"emoji"`` as an emoji on r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji["emoji"].delete()
|
||||
|
||||
"""
|
||||
url = API_PATH["emoji_delete"].format(
|
||||
emoji_name=self.name, subreddit=self.subreddit
|
||||
)
|
||||
self._reddit.delete(url)
|
||||
|
||||
@_deprecate_args("mod_flair_only", "post_flair_allowed", "user_flair_allowed")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
mod_flair_only: bool | None = None,
|
||||
post_flair_allowed: bool | None = None,
|
||||
user_flair_allowed: bool | None = None,
|
||||
):
|
||||
"""Update the permissions of an emoji in this subreddit.
|
||||
|
||||
:param mod_flair_only: Indicate whether the emoji is restricted to mod use only.
|
||||
Respects pre-existing settings if not provided.
|
||||
:param post_flair_allowed: Indicate whether the emoji may appear in post flair.
|
||||
Respects pre-existing settings if not provided.
|
||||
:param user_flair_allowed: Indicate whether the emoji may appear in user flair.
|
||||
Respects pre-existing settings if not provided.
|
||||
|
||||
.. note::
|
||||
|
||||
In order to retain pre-existing values for those that are not explicitly
|
||||
passed, a network request is issued. To avoid that network request,
|
||||
explicitly provide all values.
|
||||
|
||||
To restrict the emoji ``"emoji"`` in r/test to mod use only, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji["emoji"].update(mod_flair_only=True)
|
||||
|
||||
"""
|
||||
locals_reference = locals()
|
||||
mapping = {
|
||||
attribute: locals_reference[attribute]
|
||||
for attribute in (
|
||||
"mod_flair_only",
|
||||
"post_flair_allowed",
|
||||
"user_flair_allowed",
|
||||
)
|
||||
}
|
||||
if all(value is None for value in mapping.values()):
|
||||
msg = "At least one attribute must be provided"
|
||||
raise TypeError(msg)
|
||||
|
||||
data = {"name": self.name}
|
||||
for attribute, value in mapping.items():
|
||||
if value is None:
|
||||
value = getattr(self, attribute) # noqa: PLW2901
|
||||
data[attribute] = value
|
||||
url = API_PATH["emoji_update"].format(subreddit=self.subreddit)
|
||||
self._reddit.post(url, data=data)
|
||||
for attribute, value in data.items():
|
||||
setattr(self, attribute, value)
|
||||
|
||||
|
||||
class SubredditEmoji:
|
||||
"""Provides a set of functions to a :class:`.Subreddit` for emoji."""
|
||||
|
||||
def __getitem__(self, name: str) -> Emoji:
|
||||
"""Lazily return the :class:`.Emoji` for the subreddit named ``name``.
|
||||
|
||||
:param name: The name of the emoji.
|
||||
|
||||
This method is to be used to fetch a specific emoji url, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
emoji = reddit.subreddit("test").emoji["emoji"]
|
||||
print(emoji)
|
||||
|
||||
"""
|
||||
return Emoji(self._reddit, self.subreddit, name)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditEmoji` instance.
|
||||
|
||||
:param subreddit: The subreddit whose emoji are affected.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> list[Emoji]:
|
||||
"""Return a list of :class:`.Emoji` for the subreddit.
|
||||
|
||||
This method is to be used to discover all emoji for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for emoji in reddit.subreddit("test").emoji:
|
||||
print(emoji)
|
||||
|
||||
"""
|
||||
response = self._reddit.get(
|
||||
API_PATH["emoji_list"].format(subreddit=self.subreddit)
|
||||
)
|
||||
subreddit_keys = [
|
||||
key
|
||||
for key in response
|
||||
if key.startswith(self._reddit.config.kinds["subreddit"])
|
||||
]
|
||||
assert len(subreddit_keys) == 1
|
||||
for emoji_name, emoji_data in response[subreddit_keys[0]].items():
|
||||
yield Emoji(self._reddit, self.subreddit, emoji_name, _data=emoji_data)
|
||||
|
||||
def add(
|
||||
self,
|
||||
*,
|
||||
image_path: str,
|
||||
mod_flair_only: bool | None = None,
|
||||
name: str,
|
||||
post_flair_allowed: bool | None = None,
|
||||
user_flair_allowed: bool | None = None,
|
||||
) -> Emoji:
|
||||
"""Add an emoji to this subreddit.
|
||||
|
||||
:param image_path: A path to a jpeg or png image.
|
||||
:param mod_flair_only: When provided, indicate whether the emoji is restricted
|
||||
to mod use only (default: ``None``).
|
||||
:param name: The name of the emoji.
|
||||
:param post_flair_allowed: When provided, indicate whether the emoji may appear
|
||||
in post flair (default: ``None``).
|
||||
:param user_flair_allowed: When provided, indicate whether the emoji may appear
|
||||
in user flair (default: ``None``).
|
||||
|
||||
:returns: The :class:`.Emoji` added.
|
||||
|
||||
To add ``"emoji"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").emoji.add(name="emoji", image_path="emoji.png")
|
||||
|
||||
"""
|
||||
file = Path(image_path)
|
||||
data = {
|
||||
"filepath": file.name,
|
||||
"mimetype": "image/jpeg",
|
||||
}
|
||||
if image_path.lower().endswith(".png"):
|
||||
data["mimetype"] = "image/png"
|
||||
url = API_PATH["emoji_lease"].format(subreddit=self.subreddit)
|
||||
|
||||
# until we learn otherwise, assume this request always succeeds
|
||||
upload_lease = self._reddit.post(url, data=data)["s3UploadLease"]
|
||||
upload_data = {item["name"]: item["value"] for item in upload_lease["fields"]}
|
||||
upload_url = f"https:{upload_lease['action']}"
|
||||
|
||||
with file.open("rb") as image:
|
||||
response = self._reddit._core._requestor._http.post(
|
||||
upload_url, data=upload_data, files={"file": image}
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
data = {
|
||||
"mod_flair_only": mod_flair_only,
|
||||
"name": name,
|
||||
"post_flair_allowed": post_flair_allowed,
|
||||
"s3_key": upload_data["key"],
|
||||
"user_flair_allowed": user_flair_allowed,
|
||||
}
|
||||
url = API_PATH["emoji_upload"].format(subreddit=self.subreddit)
|
||||
self._reddit.post(url, data=data)
|
||||
return Emoji(self._reddit, self.subreddit, name)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Provide classes related to inline media."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..util import _deprecate_args
|
||||
|
||||
|
||||
class InlineMedia:
|
||||
"""Provides a way to embed media in self posts."""
|
||||
|
||||
TYPE = None
|
||||
|
||||
def __eq__(self, other: InlineMedia) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
return all(
|
||||
getattr(self, attr) == getattr(other, attr)
|
||||
for attr in ["TYPE", "path", "caption", "media_id"]
|
||||
)
|
||||
|
||||
@_deprecate_args("path", "caption")
|
||||
def __init__(self, *, caption: str = None, path: str):
|
||||
"""Initialize an :class:`.InlineMedia` instance.
|
||||
|
||||
:param caption: An optional caption to add to the image (default: ``None``).
|
||||
:param path: The path to a media file.
|
||||
|
||||
"""
|
||||
self.path = path
|
||||
self.caption = caption
|
||||
self.media_id = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"<{self.__class__.__name__} caption={self.caption!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the media in Markdown format."""
|
||||
return f'\n\n\n\n'
|
||||
|
||||
|
||||
class InlineGif(InlineMedia):
|
||||
"""Class to provide a gif to embed in text."""
|
||||
|
||||
TYPE = "gif"
|
||||
|
||||
|
||||
class InlineImage(InlineMedia):
|
||||
"""Class to provide am image to embed in text."""
|
||||
|
||||
TYPE = "img"
|
||||
|
||||
|
||||
class InlineVideo(InlineMedia):
|
||||
"""Class to provide a video to embed in text."""
|
||||
|
||||
TYPE = "video"
|
||||
804
backend/venv/Lib/site-packages/praw/models/reddit/live.py
Normal file
804
backend/venv/Lib/site-packages/praw/models/reddit/live.py
Normal file
@@ -0,0 +1,804 @@
|
||||
"""Provide the LiveThread class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterable, Iterator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..list.redditor import RedditorList
|
||||
from ..listing.generator import ListingGenerator
|
||||
from ..util import stream_generator
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class LiveContributorRelationship:
|
||||
"""Provide methods to interact with live threads' contributors."""
|
||||
|
||||
@staticmethod
|
||||
def _handle_permissions(permissions: Iterable[str]) -> str:
|
||||
permissions = {"all"} if permissions is None else set(permissions)
|
||||
return ",".join(f"+{x}" for x in permissions)
|
||||
|
||||
def __call__(self) -> list[praw.models.Redditor]:
|
||||
"""Return a :class:`.RedditorList` for live threads' contributors.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for contributor in thread.contributor():
|
||||
print(contributor)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_contributors"].format(id=self.thread.id)
|
||||
temp = self.thread._reddit.get(url)
|
||||
return temp if isinstance(temp, RedditorList) else temp[0]
|
||||
|
||||
def __init__(self, thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveContributorRelationship` instance.
|
||||
|
||||
:param thread: An instance of :class:`.LiveThread`.
|
||||
|
||||
.. note::
|
||||
|
||||
This class should not be initialized directly. Instead, obtain an instance
|
||||
via: :meth:`.LiveThread.contributor`.
|
||||
|
||||
"""
|
||||
self.thread = thread
|
||||
|
||||
def accept_invite(self):
|
||||
"""Accept an invite to contribute the live thread.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contributor.accept_invite()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_accept_invite"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def invite(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Invite a redditor to be a contributor of the live thread.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant. An empty list
|
||||
``[]`` indicates no permissions, and when not provided (``None``), indicates
|
||||
full permissions.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if the invitation already exists.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
# "manage" and "settings" permissions
|
||||
thread.contributor.invite(redditor, permissions=["manage", "settings"])
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.LiveContributorRelationship.remove_invite` to remove the invite for
|
||||
redditor.
|
||||
|
||||
"""
|
||||
url = API_PATH["live_invite"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor_invite",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
def leave(self):
|
||||
"""Abdicate the live thread contributor position (use with care).
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contributor.leave()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_leave"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
def remove(self, redditor: str | praw.models.Redditor):
|
||||
"""Remove the redditor from the live thread contributors.
|
||||
|
||||
:param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
|
||||
instance.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
thread.contributor.remove(redditor)
|
||||
thread.contributor.remove("t2_1w72") # with fullname
|
||||
|
||||
"""
|
||||
fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
|
||||
data = {"id": fullname}
|
||||
url = API_PATH["live_remove_contrib"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
def remove_invite(self, redditor: str | praw.models.Redditor):
|
||||
"""Remove the invite for redditor.
|
||||
|
||||
:param redditor: A redditor fullname (e.g., ``"t2_1w72"``) or :class:`.Redditor`
|
||||
instance.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
redditor = reddit.redditor("spez")
|
||||
thread.contributor.remove_invite(redditor)
|
||||
thread.contributor.remove_invite("t2_1w72") # with fullname
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.LiveContributorRelationship.invite` to invite a redditor to be a
|
||||
contributor of the live thread.
|
||||
|
||||
"""
|
||||
fullname = redditor.fullname if isinstance(redditor, Redditor) else redditor
|
||||
data = {"id": fullname}
|
||||
url = API_PATH["live_remove_invite"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def update(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Update the contributor permissions for ``redditor``.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant (other
|
||||
permissions are removed). An empty list ``[]`` indicates no permissions, and
|
||||
when not provided (``None``), indicates full permissions.
|
||||
|
||||
For example, to grant all permissions to the contributor, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contributor.update("spez")
|
||||
|
||||
To grant ``"access"`` and ``"edit"`` permissions (and to remove other
|
||||
permissions), try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update("spez", permissions=["access", "edit"])
|
||||
|
||||
To remove all permissions from the contributor, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit.moderator.update("spez", permissions=[])
|
||||
|
||||
"""
|
||||
url = API_PATH["live_update_perms"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
@_deprecate_args("redditor", "permissions")
|
||||
def update_invite(
|
||||
self,
|
||||
redditor: str | praw.models.Redditor,
|
||||
*,
|
||||
permissions: list[str] | None = None,
|
||||
):
|
||||
"""Update the contributor invite permissions for ``redditor``.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
:param permissions: When provided (not ``None``), permissions should be a list
|
||||
of strings specifying which subset of permissions to grant (other
|
||||
permissions are removed). An empty list ``[]`` indicates no permissions, and
|
||||
when not provided (``None``), indicates full permissions.
|
||||
|
||||
For example, to set all permissions to the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contributor.update_invite("spez")
|
||||
|
||||
To set ``"access"`` and ``"edit"`` permissions (and to remove other permissions)
|
||||
to the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update_invite("spez", permissions=["access", "edit"])
|
||||
|
||||
To remove all permissions from the invitation, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread.contributor.update_invite("spez", permissions=[])
|
||||
|
||||
"""
|
||||
url = API_PATH["live_update_perms"].format(id=self.thread.id)
|
||||
data = {
|
||||
"name": str(redditor),
|
||||
"type": "liveupdate_contributor_invite",
|
||||
"permissions": self._handle_permissions(permissions),
|
||||
}
|
||||
self.thread._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class LiveThread(RedditBase):
|
||||
"""An individual :class:`.LiveThread` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== =========================================================
|
||||
Attribute Description
|
||||
==================== =========================================================
|
||||
``created_utc`` The creation time of the live thread, in `Unix Time`_.
|
||||
``description`` Description of the live thread, as Markdown.
|
||||
``description_html`` Description of the live thread, as HTML.
|
||||
``id`` The ID of the live thread.
|
||||
``nsfw`` A ``bool`` representing whether or not the live thread is
|
||||
marked as NSFW.
|
||||
==================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@cachedproperty
|
||||
def contrib(self) -> praw.models.reddit.live.LiveThreadContribution:
|
||||
"""Provide an instance of :class:`.LiveThreadContribution`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.add("### update")
|
||||
|
||||
"""
|
||||
return LiveThreadContribution(self)
|
||||
|
||||
@cachedproperty
|
||||
def contributor(self) -> praw.models.reddit.live.LiveContributorRelationship:
|
||||
"""Provide an instance of :class:`.LiveContributorRelationship`.
|
||||
|
||||
You can call the instance to get a list of contributors which is represented as
|
||||
:class:`.RedditorList` instance consists of :class:`.Redditor` instances. Those
|
||||
:class:`.Redditor` instances have ``permissions`` attributes as contributors:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for contributor in thread.contributor():
|
||||
# prints `Redditor(name="Acidtwist") ["all"]`
|
||||
print(contributor, contributor.permissions)
|
||||
|
||||
"""
|
||||
return LiveContributorRelationship(self)
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> praw.models.reddit.live.LiveThreadStream:
|
||||
"""Provide an instance of :class:`.LiveThreadStream`.
|
||||
|
||||
Streams are used to indefinitely retrieve new updates made to a live thread,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
Updates are yielded oldest first as :class:`.LiveUpdate`. Up to 100 historical
|
||||
updates will initially be returned. To only retrieve new updates starting from
|
||||
when the stream is created, pass ``skip_existing=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
live_thread = reddit.live("ta535s1hq2je")
|
||||
for live_update in live_thread.stream.updates(skip_existing=True):
|
||||
print(live_update.author)
|
||||
|
||||
"""
|
||||
return LiveThreadStream(self)
|
||||
|
||||
def __eq__(self, other: str | praw.models.LiveThread) -> bool:
|
||||
"""Return whether the other instance equals the current.
|
||||
|
||||
.. note::
|
||||
|
||||
This comparison is case sensitive.
|
||||
|
||||
"""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
return isinstance(other, self.__class__) and str(self) == str(other)
|
||||
|
||||
def __getitem__(self, update_id: str) -> praw.models.LiveUpdate:
|
||||
"""Return a lazy :class:`.LiveUpdate` instance.
|
||||
|
||||
:param update_id: A live update ID, e.g.,
|
||||
``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.thread # LiveThread(id="ukaeu1ik4sw5")
|
||||
update.id # "7827987a-c998-11e4-a0b9-22000b6a88d2"
|
||||
update.author # "umbrae"
|
||||
|
||||
"""
|
||||
return LiveUpdate(self._reddit, self.id, update_id)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.LiveThread` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``
|
||||
|
||||
"""
|
||||
if (id, _data).count(None) != 1:
|
||||
msg = "Either 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
if id:
|
||||
self.id = id
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "liveabout", {"id": self.id}, None
|
||||
|
||||
def discussions(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Submission]:
|
||||
"""Get submissions linking to the thread.
|
||||
|
||||
:param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
|
||||
constructor.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.Submission`
|
||||
objects.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
for submission in thread.discussions(limit=None):
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_discussions"].format(id=self.id)
|
||||
return ListingGenerator(self._reddit, url, **generator_kwargs)
|
||||
|
||||
def report(self, type: str):
|
||||
"""Report the thread violating the Reddit rules.
|
||||
|
||||
:param type: One of ``"spam"``, ``"vote-manipulation"``,
|
||||
``"personal-information"``, ``"sexualizing-minors"``, or
|
||||
``"site-breaking"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
thread.report("spam")
|
||||
|
||||
"""
|
||||
url = API_PATH["live_report"].format(id=self.id)
|
||||
self._reddit.post(url, data={"type": type})
|
||||
|
||||
def updates(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.LiveUpdate]:
|
||||
"""Return a :class:`.ListingGenerator` yields :class:`.LiveUpdate` s.
|
||||
|
||||
:param generator_kwargs: keyword arguments passed to :class:`.ListingGenerator`
|
||||
constructor.
|
||||
|
||||
:returns: A :class:`.ListingGenerator` object which yields :class:`.LiveUpdate`
|
||||
objects.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
after = "LiveUpdate_fefb3dae-7534-11e6-b259-0ef8c7233633"
|
||||
for submission in thread.updates(limit=5, params={"after": after}):
|
||||
print(submission.body)
|
||||
|
||||
"""
|
||||
url = API_PATH["live_updates"].format(id=self.id)
|
||||
for update in ListingGenerator(self._reddit, url, **generator_kwargs):
|
||||
update._thread = self
|
||||
yield update
|
||||
|
||||
|
||||
class LiveThreadContribution:
|
||||
"""Provides a set of contribution functions to a :class:`.LiveThread`."""
|
||||
|
||||
def __init__(self, thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveThreadContribution` instance.
|
||||
|
||||
:param thread: An instance of :class:`.LiveThread`.
|
||||
|
||||
This instance can be retrieved through ``thread.contrib`` where thread is a
|
||||
:class:`.LiveThread` instance. E.g.,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.add("### update")
|
||||
|
||||
"""
|
||||
self.thread = thread
|
||||
|
||||
def add(self, body: str):
|
||||
"""Add an update to the live thread.
|
||||
|
||||
:param body: The Markdown formatted content for the update.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
thread.contrib.add("test `LiveThreadContribution.add()`")
|
||||
|
||||
"""
|
||||
url = API_PATH["live_add_update"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url, data={"body": body})
|
||||
|
||||
def close(self):
|
||||
"""Close the live thread permanently (cannot be undone).
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
thread.contrib.close()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_close"].format(id=self.thread.id)
|
||||
self.thread._reddit.post(url)
|
||||
|
||||
@_deprecate_args("title", "description", "nsfw", "resources")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
description: str | None = None,
|
||||
nsfw: bool | None = None,
|
||||
resources: str | None = None,
|
||||
title: str | None = None,
|
||||
**other_settings: str | None,
|
||||
):
|
||||
"""Update settings of the live thread.
|
||||
|
||||
:param description: The live thread's description (default: ``None``).
|
||||
:param nsfw: Indicate whether this thread is not safe for work (default:
|
||||
``None``).
|
||||
:param resources: Markdown formatted information that is useful for the live
|
||||
thread (default: ``None``).
|
||||
:param title: The title of the live thread (default: ``None``).
|
||||
|
||||
Does nothing if no arguments are provided.
|
||||
|
||||
Each setting will maintain its current value if ``None`` is specified.
|
||||
|
||||
Additional keyword arguments can be provided to handle new settings as Reddit
|
||||
introduces them.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
|
||||
# update 'title' and 'nsfw'
|
||||
updated_thread = thread.contrib.update(title=new_title, nsfw=True)
|
||||
|
||||
If Reddit introduces new settings, you must specify ``None`` for the setting you
|
||||
want to maintain:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# update 'nsfw' and maintain new setting 'foo'
|
||||
thread.contrib.update(nsfw=True, foo=None)
|
||||
|
||||
"""
|
||||
settings = {
|
||||
"title": title,
|
||||
"description": description,
|
||||
"nsfw": nsfw,
|
||||
"resources": resources,
|
||||
}
|
||||
settings.update(other_settings)
|
||||
if all(value is None for value in settings.values()):
|
||||
return
|
||||
# get settings from Reddit (not cache)
|
||||
thread = LiveThread(self.thread._reddit, self.thread.id)
|
||||
data = {
|
||||
key: getattr(thread, key) if value is None else value
|
||||
for key, value in settings.items()
|
||||
}
|
||||
|
||||
url = API_PATH["live_update_thread"].format(id=self.thread.id)
|
||||
# prawcore (0.7.0) Session.request() modifies `data` kwarg
|
||||
self.thread._reddit.post(url, data=data.copy())
|
||||
self.thread._reset_attributes(*data.keys())
|
||||
|
||||
|
||||
class LiveThreadStream:
|
||||
"""Provides a :class:`.LiveThread` stream.
|
||||
|
||||
Usually used via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, live_thread: praw.models.LiveThread):
|
||||
"""Initialize a :class:`.LiveThreadStream` instance.
|
||||
|
||||
:param live_thread: The live thread associated with the stream.
|
||||
|
||||
"""
|
||||
self.live_thread = live_thread
|
||||
|
||||
def updates(
|
||||
self, **stream_options: dict[str, Any]
|
||||
) -> Iterator[praw.models.LiveUpdate]:
|
||||
"""Yield new updates to the live thread as they become available.
|
||||
|
||||
:param skip_existing: Set to ``True`` to only fetch items created after the
|
||||
stream (default: ``False``).
|
||||
|
||||
As with :meth:`.LiveThread.updates()`, updates are yielded as
|
||||
:class:`.LiveUpdate`.
|
||||
|
||||
Updates are yielded oldest first. Up to 100 historical updates will initially be
|
||||
returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new updates made to the ``"ta535s1hq2je"`` live
|
||||
thread, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for live_update in reddit.live("ta535s1hq2je").stream.updates():
|
||||
print(live_update.body)
|
||||
|
||||
To only retrieve new updates starting from when the stream is created, pass
|
||||
``skip_existing=True``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
live_thread = reddit.live("ta535s1hq2je")
|
||||
for live_update in live_thread.stream.updates(skip_existing=True):
|
||||
print(live_update.author)
|
||||
|
||||
"""
|
||||
return stream_generator(self.live_thread.updates, **stream_options)
|
||||
|
||||
|
||||
class LiveUpdateContribution:
|
||||
"""Provides a set of contribution functions to :class:`.LiveUpdate`."""
|
||||
|
||||
def __init__(self, update: praw.models.LiveUpdate):
|
||||
"""Initialize a :class:`.LiveUpdateContribution` instance.
|
||||
|
||||
:param update: An instance of :class:`.LiveUpdate`.
|
||||
|
||||
This instance can be retrieved through ``update.contrib`` where update is a
|
||||
:class:`.LiveUpdate` instance. E.g.,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.contrib # LiveUpdateContribution instance
|
||||
update.contrib.remove()
|
||||
|
||||
"""
|
||||
self.update = update
|
||||
|
||||
def remove(self):
|
||||
"""Remove a live update.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ydwwxneu7vsa")
|
||||
update = thread["6854605a-efec-11e6-b0c7-0eafac4ff094"]
|
||||
update.contrib.remove()
|
||||
|
||||
"""
|
||||
url = API_PATH["live_remove_update"].format(id=self.update.thread.id)
|
||||
data = {"id": self.update.fullname}
|
||||
self.update.thread._reddit.post(url, data=data)
|
||||
|
||||
def strike(self):
|
||||
"""Strike a content of a live update.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("xyu8kmjvfrww")
|
||||
update = thread["cb5fe532-dbee-11e6-9a91-0e6d74fabcc4"]
|
||||
update.contrib.strike()
|
||||
|
||||
To check whether the update is stricken or not, use ``update.stricken``
|
||||
attribute.
|
||||
|
||||
.. note::
|
||||
|
||||
Accessing lazy attributes on updates (includes ``update.stricken``) may
|
||||
raise :py:class:`AttributeError`. See :class:`.LiveUpdate` for details.
|
||||
|
||||
"""
|
||||
url = API_PATH["live_strike"].format(id=self.update.thread.id)
|
||||
data = {"id": self.update.fullname}
|
||||
self.update.thread._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class LiveUpdate(FullnameMixin, RedditBase):
|
||||
"""An individual :class:`.LiveUpdate` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ===================================================================
|
||||
Attribute Description
|
||||
=============== ===================================================================
|
||||
``author`` The :class:`.Redditor` who made the update.
|
||||
``body`` Body of the update, as Markdown.
|
||||
``body_html`` Body of the update, as HTML.
|
||||
``created_utc`` The time the update was created, as `Unix Time`_.
|
||||
``stricken`` A ``bool`` representing whether or not the update was stricken (see
|
||||
:meth:`.strike`).
|
||||
=============== ===================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
_kind = "LiveUpdate"
|
||||
|
||||
@cachedproperty
|
||||
def contrib(self) -> praw.models.reddit.live.LiveUpdateContribution:
|
||||
"""Provide an instance of :class:`.LiveUpdateContribution`.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
thread = reddit.live("ukaeu1ik4sw5")
|
||||
update = thread["7827987a-c998-11e4-a0b9-22000b6a88d2"]
|
||||
update.contrib # LiveUpdateContribution instance
|
||||
|
||||
"""
|
||||
return LiveUpdateContribution(self)
|
||||
|
||||
@property
|
||||
def thread(self) -> LiveThread:
|
||||
"""Return :class:`.LiveThread` object the update object belongs to."""
|
||||
return self._thread
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
thread_id: str | None = None,
|
||||
update_id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.LiveUpdate` instance.
|
||||
|
||||
Either ``thread_id`` and ``update_id``, or ``_data`` must be provided.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param thread_id: A live thread ID, e.g., ``"ukaeu1ik4sw5"``.
|
||||
:param update_id: A live update ID, e.g.,
|
||||
``"7827987a-c998-11e4-a0b9-22000b6a88d2"``.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
update = LiveUpdate(reddit, "ukaeu1ik4sw5", "7827987a-c998-11e4-a0b9-22000b6a88d2")
|
||||
update.thread # LiveThread(id="ukaeu1ik4sw5")
|
||||
update.id # "7827987a-c998-11e4-a0b9-22000b6a88d2"
|
||||
update.author # "umbrae"
|
||||
|
||||
"""
|
||||
if _data is not None:
|
||||
# Since _data (part of JSON returned from reddit) have no thread ID,
|
||||
# self._thread must be set by the caller of LiveUpdate(). See the code of
|
||||
# LiveThread.updates() for example.
|
||||
super().__init__(reddit, _data=_data, _fetched=True)
|
||||
elif thread_id and update_id:
|
||||
self.id = update_id
|
||||
super().__init__(reddit, _data=None)
|
||||
self._thread = LiveThread(self._reddit, thread_id)
|
||||
else:
|
||||
msg = "Either 'thread_id' and 'update_id', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify author."""
|
||||
if attribute == "author":
|
||||
value = Redditor(self._reddit, name=value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _fetch(self):
|
||||
url = API_PATH["live_focus"].format(thread_id=self.thread.id, update_id=self.id)
|
||||
other = self._reddit.get(url)[0]
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
175
backend/venv/Lib/site-packages/praw/models/reddit/message.py
Normal file
175
backend/venv/Lib/site-packages/praw/models/reddit/message.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""Provide the Message class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, InboxableMixin, ReplyableMixin
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Message(InboxableMixin, ReplyableMixin, FullnameMixin, RedditBase):
|
||||
"""A class for private messages.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ================================================================
|
||||
Attribute Description
|
||||
=============== ================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the message, as Markdown.
|
||||
``body_html`` The body of the message, as HTML.
|
||||
``created_utc`` Time the message was created, represented in `Unix Time`_.
|
||||
``dest`` Provides an instance of :class:`.Redditor`. The recipient of the
|
||||
message.
|
||||
``id`` The ID of the message.
|
||||
``name`` The full ID of the message, prefixed with ``t4_``.
|
||||
``subject`` The subject of the message.
|
||||
``was_comment`` Whether or not the message was a comment reply.
|
||||
=============== ================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls, data: dict[str, Any], reddit: praw.Reddit
|
||||
) -> Message | SubredditMessage:
|
||||
"""Return an instance of :class:`.Message` or :class:`.SubredditMessage` from ``data``.
|
||||
|
||||
:param data: The structured data.
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
if data["author"]:
|
||||
data["author"] = Redditor(reddit, data["author"])
|
||||
|
||||
if data["dest"].startswith("#"):
|
||||
data["dest"] = Subreddit(reddit, data["dest"][1:])
|
||||
else:
|
||||
data["dest"] = Redditor(reddit, data["dest"])
|
||||
|
||||
if data["replies"]:
|
||||
replies = data["replies"]
|
||||
data["replies"] = reddit._objector.objectify(replies["data"]["children"])
|
||||
else:
|
||||
data["replies"] = []
|
||||
|
||||
if data["subreddit"]:
|
||||
data["subreddit"] = Subreddit(reddit, data["subreddit"])
|
||||
return SubredditMessage(reddit, _data=data)
|
||||
|
||||
return cls(reddit, _data=data)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["message"]
|
||||
|
||||
@property
|
||||
def parent(self) -> praw.models.Message | None:
|
||||
"""Return the parent of the message if it exists."""
|
||||
if not self._parent and self.parent_id:
|
||||
self._parent = self._reddit.inbox.message(self.parent_id.split("_")[1])
|
||||
return self._parent
|
||||
|
||||
@parent.setter
|
||||
def parent(self, value: praw.models.Message | None):
|
||||
self._parent = value
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.Message` instance."""
|
||||
super().__init__(reddit, _data=_data, _fetched=True)
|
||||
self._parent = None
|
||||
for reply in _data.get("replies", []):
|
||||
if reply.parent_id == self.fullname:
|
||||
reply.parent = self
|
||||
|
||||
def delete(self):
|
||||
"""Delete the message.
|
||||
|
||||
.. note::
|
||||
|
||||
Reddit does not return an indication of whether or not the message was
|
||||
successfully deleted.
|
||||
|
||||
For example, to delete the most recent message in your inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
next(reddit.inbox.all()).delete()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["delete_message"], data={"id": self.fullname})
|
||||
|
||||
|
||||
class SubredditMessage(Message):
|
||||
"""A class for messages to a subreddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== =================================================================
|
||||
Attribute Description
|
||||
=============== =================================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``body`` The body of the message, as Markdown.
|
||||
``body_html`` The body of the message, as HTML.
|
||||
``created_utc`` Time the message was created, represented in `Unix Time`_.
|
||||
``dest`` Provides an instance of :class:`.Redditor`. The recipient of the
|
||||
message.
|
||||
``id`` The ID of the message.
|
||||
``name`` The full ID of the message, prefixed with ``t4_``.
|
||||
``subject`` The subject of the message.
|
||||
``subreddit`` If the message was sent from a subreddit, provides an instance of
|
||||
:class:`.Subreddit`.
|
||||
``was_comment`` Whether or not the message was a comment reply.
|
||||
=============== =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
def mute(self):
|
||||
"""Mute the sender of this :class:`.SubredditMessage`.
|
||||
|
||||
For example, to mute the sender of the first :class:`.SubredditMessage` in the
|
||||
authenticated users' inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
msg = next(
|
||||
message for message in reddit.inbox.all() if isinstance(message, SubredditMessage)
|
||||
)
|
||||
msg.mute()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["mute_sender"], data={"id": self.fullname})
|
||||
|
||||
def unmute(self):
|
||||
"""Unmute the sender of this :class:`.SubredditMessage`.
|
||||
|
||||
For example, to unmute the sender of the first :class:`.SubredditMessage` in the
|
||||
authenticated users' inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
msg = next(
|
||||
message for message in reddit.inbox.all() if isinstance(message, SubredditMessage)
|
||||
)
|
||||
msg.unmute()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unmute_sender"], data={"id": self.fullname})
|
||||
@@ -0,0 +1,322 @@
|
||||
"""Package providing reddit class mixins."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
from .editable import EditableMixin
|
||||
from .fullname import FullnameMixin
|
||||
from .gildable import GildableMixin
|
||||
from .inboxable import InboxableMixin
|
||||
from .inboxtoggleable import InboxToggleableMixin
|
||||
from .messageable import MessageableMixin
|
||||
from .modnote import ModNoteMixin
|
||||
from .replyable import ReplyableMixin
|
||||
from .reportable import ReportableMixin
|
||||
from .savable import SavableMixin
|
||||
from .votable import VotableMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ThingModerationMixin(ModNoteMixin):
|
||||
r"""Provides moderation methods for :class:`.Comment`\ s and :class:`.Submission`\ s."""
|
||||
|
||||
REMOVAL_MESSAGE_API = None
|
||||
|
||||
def _add_removal_reason(self, *, mod_note: str = "", reason_id: str | None = None):
|
||||
"""Add a removal reason for a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param mod_note: A message for the other moderators.
|
||||
:param reason_id: The removal reason ID.
|
||||
|
||||
It is necessary to first call :meth:`.remove` on the :class:`.Comment` or
|
||||
:class:`.Submission`.
|
||||
|
||||
If ``reason_id`` is not specified, ``mod_note`` cannot be blank.
|
||||
|
||||
"""
|
||||
if not reason_id and not mod_note:
|
||||
msg = "mod_note cannot be blank if reason_id is not specified"
|
||||
raise ValueError(msg)
|
||||
# Only the first element of the item_id list is used.
|
||||
data = {
|
||||
"item_ids": [self.thing.fullname],
|
||||
"mod_note": mod_note,
|
||||
"reason_id": reason_id,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["removal_reasons"], data={"json": dumps(data)})
|
||||
|
||||
def approve(self):
|
||||
"""Approve a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Approving a comment or submission reverts a removal, resets the report counter,
|
||||
adds a green check mark indicator (only visible to other moderators) on the
|
||||
website view, and sets the ``approved_by`` attribute to the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# approve a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.approve()
|
||||
# approve a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["approve"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("how", "sticky")
|
||||
def distinguish(self, *, how: str = "yes", sticky: bool = False):
|
||||
"""Distinguish a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param how: One of ``"yes"``, ``"no"``, ``"admin"``, or ``"special"``. ``"yes"``
|
||||
adds a moderator level distinguish. ``"no"`` removes any distinction.
|
||||
``"admin"`` and ``"special"`` require special user privileges to use
|
||||
(default ``"yes"``).
|
||||
:param sticky: :class:`.Comment` is stickied if ``True``, placing it at the top
|
||||
of the comment page regardless of score. If thing is not a top-level
|
||||
comment, this parameter is silently ignored (default ``False``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# distinguish and sticky a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.distinguish(sticky=True)
|
||||
# undistinguish a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.distinguish(how="no")
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.undistinguish`
|
||||
|
||||
"""
|
||||
data = {"how": how, "id": self.thing.fullname}
|
||||
if sticky and getattr(self.thing, "is_root", False):
|
||||
data["sticky"] = True
|
||||
self.thing._reddit.post(API_PATH["distinguish"], data=data)
|
||||
|
||||
def ignore_reports(self):
|
||||
"""Ignore future reports on a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Calling this method will prevent future reports on this :class:`.Comment` or
|
||||
:class:`.Submission` from both triggering notifications and appearing in the
|
||||
various moderation listings. The report count will still increment on the
|
||||
:class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# ignore future reports on a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.ignore_reports()
|
||||
# ignore future reports on a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.ignore_reports()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unignore_reports`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["ignore_reports"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def lock(self):
|
||||
"""Lock a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# lock a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.lock()
|
||||
# lock a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.lock()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unlock`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["lock"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("spam", "mod_note", "reason_id")
|
||||
def remove(
|
||||
self, *, mod_note: str = "", spam: bool = False, reason_id: str | None = None
|
||||
):
|
||||
"""Remove a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
:param mod_note: A message for the other moderators.
|
||||
:param spam: When ``True``, use the removal to help train the
|
||||
:class:`.Subreddit`'s spam filter (default: ``False``).
|
||||
:param reason_id: The removal reason ID.
|
||||
|
||||
If either ``reason_id`` or ``mod_note`` are provided, a second API call is made
|
||||
to add the removal reason.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# remove a comment and mark as spam:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.remove(spam=True)
|
||||
# remove a submission
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.remove()
|
||||
# remove a submission with a removal reason
|
||||
reason = reddit.subreddit.mod.removal_reasons["110ni21zo23ql"]
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.remove(reason_id=reason.id)
|
||||
|
||||
"""
|
||||
data = {"id": self.thing.fullname, "spam": bool(spam)}
|
||||
self.thing._reddit.post(API_PATH["remove"], data=data)
|
||||
if any([reason_id, mod_note]):
|
||||
self._add_removal_reason(mod_note=mod_note, reason_id=reason_id)
|
||||
|
||||
@_deprecate_args("message", "title", "type")
|
||||
def send_removal_message(
|
||||
self,
|
||||
*,
|
||||
message: str,
|
||||
title: str = "ignored",
|
||||
type: str = "public",
|
||||
) -> praw.models.Comment | None:
|
||||
"""Send a removal message for a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The object has to be removed before giving it a removal reason. Remove the
|
||||
object with :meth:`.remove`. Trying to add a removal reason without removing
|
||||
the object will result in :class:`.RedditAPIException` being thrown with an
|
||||
``INVALID_ID`` error_type.
|
||||
|
||||
Reddit adds human-readable information about the object to the message.
|
||||
|
||||
:param type: One of ``"public"``, ``"public_as_subreddit"``, ``"private"``, or
|
||||
``"private_exposed"``. ``"public"`` leaves a stickied comment on the post.
|
||||
``"public_as_subreddit"`` leaves a stickied comment on the post with the
|
||||
u/subreddit-ModTeam account. ``"private"`` sends a modmail message with
|
||||
hidden username. ``"private_exposed"`` sends a modmail message without
|
||||
hidden username (default: ``"public"``).
|
||||
:param title: The short reason given in the message. Ignored if type is
|
||||
``"public"`` or ``"public_as_subreddit"``.
|
||||
:param message: The body of the message.
|
||||
|
||||
:returns: The new :class:`.Comment` if ``type`` is ``"public"`` or
|
||||
``"public_as_subreddit"``.
|
||||
|
||||
"""
|
||||
# The API endpoint used to send removal messages is different for posts and
|
||||
# comments, so the derived classes specify which one.
|
||||
if self.REMOVAL_MESSAGE_API is None:
|
||||
msg = "ThingModerationMixin must be extended."
|
||||
raise NotImplementedError(msg)
|
||||
url = API_PATH[self.REMOVAL_MESSAGE_API]
|
||||
|
||||
# Only the first element of the item_id list is used.
|
||||
data = {
|
||||
"item_id": [self.thing.fullname],
|
||||
"message": message,
|
||||
"title": title,
|
||||
"type": type,
|
||||
}
|
||||
|
||||
return self.thing._reddit.post(url, data={"json": dumps(data)}) or None
|
||||
|
||||
def undistinguish(self):
|
||||
"""Remove mod, admin, or special distinguishing from an object.
|
||||
|
||||
Also unstickies the object if applicable.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# undistinguish a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.undistinguish()
|
||||
# undistinguish a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.undistinguish()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.distinguish`
|
||||
|
||||
"""
|
||||
self.distinguish(how="no")
|
||||
|
||||
def unignore_reports(self):
|
||||
"""Resume receiving future reports on a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Future reports on this :class:`.Comment` or :class:`.Submission` will cause
|
||||
notifications, and appear in the various moderation listings.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# accept future reports on a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.unignore_reports()
|
||||
# accept future reports on a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.unignore_reports()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.ignore_reports`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["unignore_reports"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def unlock(self):
|
||||
"""Unlock a :class:`.Comment` or :class:`.Submission`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# unlock a comment:
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.mod.unlock()
|
||||
# unlock a submission:
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.unlock()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.lock`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["unlock"], data={"id": self.thing.fullname})
|
||||
|
||||
|
||||
class UserContentMixin(
|
||||
EditableMixin,
|
||||
GildableMixin,
|
||||
InboxToggleableMixin,
|
||||
ReplyableMixin,
|
||||
ReportableMixin,
|
||||
SavableMixin,
|
||||
VotableMixin,
|
||||
):
|
||||
"""A convenience mixin that applies to both Comments and Submissions."""
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Provide the EditableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class EditableMixin:
|
||||
"""Interface for classes that can be edited and deleted."""
|
||||
|
||||
def delete(self):
|
||||
"""Delete the object.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.delete()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.delete()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["del"], data={"id": self.fullname})
|
||||
|
||||
def edit(self, body: str) -> praw.models.Comment | praw.models.Submission:
|
||||
"""Replace the body of the object with ``body``.
|
||||
|
||||
:param body: The Markdown formatted content for the updated object.
|
||||
|
||||
:returns: The current instance after updating its attributes.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
|
||||
# construct the text of an edited comment
|
||||
# by appending to the old body:
|
||||
edited_body = comment.body + "Edit: thanks for the gold!"
|
||||
comment.edit(edited_body)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"text": body,
|
||||
"thing_id": self.fullname,
|
||||
"validate_on_submit": self._reddit.validate_on_submit,
|
||||
}
|
||||
updated = self._reddit.post(API_PATH["edit"], data=data)[0]
|
||||
for attribute in [
|
||||
"_fetched",
|
||||
"_reddit",
|
||||
"_submission",
|
||||
"replies",
|
||||
"subreddit",
|
||||
]:
|
||||
if attribute in updated.__dict__:
|
||||
delattr(updated, attribute)
|
||||
self.__dict__.update(updated.__dict__)
|
||||
return self
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Provide the FullnameMixin class."""
|
||||
|
||||
|
||||
class FullnameMixin:
|
||||
"""Interface for classes that have a fullname."""
|
||||
|
||||
_kind = None
|
||||
|
||||
@property
|
||||
def fullname(self) -> str:
|
||||
"""Return the object's fullname.
|
||||
|
||||
A fullname is an object's kind mapping like ``t3`` followed by an underscore and
|
||||
the object's base36 ID, e.g., ``t1_c5s96e0``.
|
||||
|
||||
"""
|
||||
if "_" in self.id:
|
||||
return self.id
|
||||
return f"{self._kind}_{self.id}"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Provide the GildableMixin class."""
|
||||
|
||||
from warnings import warn
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
|
||||
class GildableMixin:
|
||||
"""Interface for classes that can be gilded."""
|
||||
|
||||
@_deprecate_args("gild_type", "is_anonymous", "message")
|
||||
def award(
|
||||
self,
|
||||
*,
|
||||
gild_type: str = "gid_2",
|
||||
is_anonymous: bool = True,
|
||||
message: str = None,
|
||||
) -> dict:
|
||||
"""Award the author of the item.
|
||||
|
||||
:param gild_type: Type of award to give. See table below for currently know
|
||||
global award types.
|
||||
:param is_anonymous: If ``True``, the authenticated user's username will not be
|
||||
revealed to the recipient.
|
||||
:param message: Message to include with the award.
|
||||
|
||||
:returns: A dict containing info similar to what is shown below:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{
|
||||
"subreddit_balance": 85260,
|
||||
"treatment_tags": [],
|
||||
"coins": 8760,
|
||||
"gildings": {"gid_1": 0, "gid_2": 1, "gid_3": 0},
|
||||
"awarder_karma_received": 4,
|
||||
"all_awardings": [
|
||||
{
|
||||
"giver_coin_reward": 0,
|
||||
"subreddit_id": None,
|
||||
"is_new": False,
|
||||
"days_of_drip_extension": 0,
|
||||
"coin_price": 75,
|
||||
"id": "award_9663243a-e77f-44cf-abc6-850ead2cd18d",
|
||||
"penny_donate": 0,
|
||||
"coin_reward": 0,
|
||||
"icon_url": "https://www.redditstatic.com/gold/awards/icon/SnooClappingPremium_512.png",
|
||||
"days_of_premium": 0,
|
||||
"icon_height": 512,
|
||||
"tiers_by_required_awardings": None,
|
||||
"icon_width": 512,
|
||||
"static_icon_width": 512,
|
||||
"start_date": None,
|
||||
"is_enabled": True,
|
||||
"awardings_required_to_grant_benefits": None,
|
||||
"description": "For an especially amazing showing.",
|
||||
"end_date": None,
|
||||
"subreddit_coin_reward": 0,
|
||||
"count": 1,
|
||||
"static_icon_height": 512,
|
||||
"name": "Bravo Grande!",
|
||||
"icon_format": "APNG",
|
||||
"award_sub_type": "PREMIUM",
|
||||
"penny_price": 0,
|
||||
"award_type": "global",
|
||||
"static_icon_url": "https://i.redd.it/award_images/t5_q0gj4/59e02tmkl4451_BravoGrande-Static.png",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
.. warning::
|
||||
|
||||
Requires the authenticated user to own Reddit Coins. Calling this method
|
||||
will consume Reddit Coins.
|
||||
|
||||
To award the gold award anonymously do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.award()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.award()
|
||||
|
||||
To award the platinum award with the message 'Nice!' and reveal your username to
|
||||
the recipient do:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.award(gild_type="gild_3", message="Nice!", is_anonymous=False)
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.award(gild_type="gild_3", message="Nice!", is_anonymous=False)
|
||||
|
||||
.. include:: awards.txt
|
||||
|
||||
"""
|
||||
params = {
|
||||
"api_type": "json",
|
||||
"gild_type": gild_type,
|
||||
"is_anonymous": is_anonymous,
|
||||
"thing_id": self.fullname,
|
||||
"message": message,
|
||||
}
|
||||
return self._reddit.post(API_PATH["award_thing"], params=params)
|
||||
|
||||
def gild(self) -> dict:
|
||||
"""Alias for :meth:`.award` to maintain backwards compatibility."""
|
||||
warn(
|
||||
"'.gild' has been renamed to '.award'.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.award()
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Provide the InboxableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class InboxableMixin:
|
||||
"""Interface for :class:`.RedditBase` subclasses that originate from the inbox."""
|
||||
|
||||
def block(self):
|
||||
"""Block the user who sent the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.block()
|
||||
|
||||
# or, identically:
|
||||
|
||||
comment.author.block()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["block"], data={"id": self.fullname})
|
||||
|
||||
def collapse(self):
|
||||
"""Mark the item as collapsed.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox()
|
||||
|
||||
# select first inbox item and collapse it message = next(inbox)
|
||||
message.collapse()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.uncollapse`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.collapse([self])
|
||||
|
||||
def mark_read(self):
|
||||
"""Mark a single inbox item as read.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox.unread()
|
||||
|
||||
for message in inbox:
|
||||
# process unread messages
|
||||
...
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.mark_unread`
|
||||
|
||||
To mark the whole inbox as read with a single network request, use
|
||||
:meth:`.Inbox.mark_all_read`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.mark_read([self])
|
||||
|
||||
def mark_unread(self):
|
||||
"""Mark the item as unread.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox(limit=10)
|
||||
|
||||
for message in inbox:
|
||||
# process messages
|
||||
...
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.mark_read`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.mark_unread([self])
|
||||
|
||||
def unblock_subreddit(self):
|
||||
"""Unblock a subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
For example, to unblock all blocked subreddits that you can find by going
|
||||
through your inbox:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import SubredditMessage
|
||||
|
||||
subs = set()
|
||||
for item in reddit.inbox.messages(limit=None):
|
||||
if isinstance(item, SubredditMessage):
|
||||
if (
|
||||
item.subject == "[message from blocked subreddit]"
|
||||
and str(item.subreddit) not in subs
|
||||
):
|
||||
item.unblock_subreddit()
|
||||
subs.add(str(item.subreddit))
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unblock_subreddit"], data={"id": self.fullname})
|
||||
|
||||
def uncollapse(self):
|
||||
"""Mark the item as uncollapsed.
|
||||
|
||||
.. note::
|
||||
|
||||
This method pertains only to objects which were retrieved via the inbox.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
inbox = reddit.inbox()
|
||||
|
||||
# select first inbox item and uncollapse it
|
||||
message = next(inbox)
|
||||
message.uncollapse()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.collapse`
|
||||
|
||||
"""
|
||||
self._reddit.inbox.uncollapse([self])
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Provide the InboxToggleableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class InboxToggleableMixin:
|
||||
"""Interface for classes that can optionally receive inbox replies."""
|
||||
|
||||
def disable_inbox_replies(self):
|
||||
"""Disable inbox replies for the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only apply to items created by the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.disable_inbox_replies()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.disable_inbox_replies()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.enable_inbox_replies`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["sendreplies"], data={"id": self.fullname, "state": False}
|
||||
)
|
||||
|
||||
def enable_inbox_replies(self):
|
||||
"""Enable inbox replies for the item.
|
||||
|
||||
.. note::
|
||||
|
||||
This can only apply to items created by the authenticated user.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
comment = reddit.comment("dkk4qjd")
|
||||
comment.enable_inbox_replies()
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.enable_inbox_replies()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.disable_inbox_replies`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["sendreplies"], data={"id": self.fullname, "state": True}
|
||||
)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Provide the MessageableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class MessageableMixin:
|
||||
"""Interface for classes that can be messaged."""
|
||||
|
||||
@_deprecate_args("subject", "message", "from_subreddit")
|
||||
def message(
|
||||
self,
|
||||
*,
|
||||
from_subreddit: praw.models.Subreddit | str | None = None,
|
||||
message: str,
|
||||
subject: str,
|
||||
):
|
||||
"""Send a message to a :class:`.Redditor` or a :class:`.Subreddit`'s moderators (modmail).
|
||||
|
||||
:param from_subreddit: A :class:`.Subreddit` instance or string to send the
|
||||
message from. When provided, messages are sent from the subreddit rather
|
||||
than from the authenticated user.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the subreddit and have the
|
||||
``mail`` moderator permission.
|
||||
|
||||
:param message: The message content.
|
||||
:param subject: The subject of the message.
|
||||
|
||||
For example, to send a private message to u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").message(subject="TEST", message="test message from PRAW")
|
||||
|
||||
To send a message to u/spez from the moderators of r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").message(
|
||||
subject="TEST", message="test message from r/test", from_subreddit="test"
|
||||
)
|
||||
|
||||
To send a message to the moderators of r/test, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").message(subject="TEST", message="test PM from PRAW")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"subject": subject,
|
||||
"text": message,
|
||||
"to": f"{getattr(self.__class__, 'MESSAGE_PREFIX', '')}{self}",
|
||||
}
|
||||
if from_subreddit:
|
||||
data["from_sr"] = str(from_subreddit)
|
||||
self._reddit.post(API_PATH["compose"], data=data)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Provide the ModNoteMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ModNoteMixin:
|
||||
"""Interface for classes that can have a moderator note set on them."""
|
||||
|
||||
def author_notes(
|
||||
self, **generator_kwargs: Any
|
||||
) -> Generator[praw.models.ModNote, None, None]:
|
||||
"""Get the moderator notes for the author of this object in the subreddit it's posted in.
|
||||
|
||||
:param generator_kwargs: Additional keyword arguments are passed in the
|
||||
initialization of the moderator note generator.
|
||||
|
||||
:returns: A generator of :class:`.ModNote`.
|
||||
|
||||
For example, to list all notes the author of a submission, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for note in reddit.submission("92dd8").mod.author_notes():
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
return self.thing.subreddit.mod.notes.redditors(
|
||||
self.thing.author, **generator_kwargs
|
||||
)
|
||||
|
||||
def create_note(
|
||||
self, *, label: str | None = None, note: str, **other_settings: Any
|
||||
) -> praw.models.ModNote:
|
||||
"""Create a moderator note on the author of this object in the subreddit it's posted in.
|
||||
|
||||
:param label: The label for the note. As of this writing, this can be one of the
|
||||
following: ``"ABUSE_WARNING"``, ``"BAN"``, ``"BOT_BAN"``,
|
||||
``"HELPFUL_USER"``, ``"PERMA_BAN"``, ``"SOLID_CONTRIBUTOR"``,
|
||||
``"SPAM_WARNING"``, ``"SPAM_WATCH"``, or ``None`` (default: ``None``).
|
||||
:param note: The content of the note. As of this writing, this is limited to 250
|
||||
characters.
|
||||
:param other_settings: Additional keyword arguments are passed to
|
||||
:meth:`~.BaseModNotes.create`.
|
||||
|
||||
:returns: The new :class:`.ModNote` object.
|
||||
|
||||
For example, to create a note on a :class:`.Submission`, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.submission("92dd8").mod.create_note(label="HELPFUL_USER", note="Test note")
|
||||
|
||||
"""
|
||||
return self.thing.subreddit.mod.notes.create(
|
||||
label=label, note=note, thing=self.thing, **other_settings
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Provide the ReplyableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class ReplyableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be replied to."""
|
||||
|
||||
def reply(self, body: str) -> praw.models.Comment | praw.models.Message | None:
|
||||
"""Reply to the object.
|
||||
|
||||
:param body: The Markdown formatted content for a comment.
|
||||
|
||||
:returns: A :class:`.Comment` or :class:`.Message` object for the newly created
|
||||
comment or message or ``None`` if Reddit doesn't provide one.
|
||||
|
||||
:raises: ``prawcore.exceptions.Forbidden`` when attempting to reply to some
|
||||
items, such as locked submissions/comments or non-replyable messages.
|
||||
|
||||
A ``None`` value can be returned if the target is a comment or submission in a
|
||||
quarantined subreddit and the authenticated user has not opt-ed into viewing the
|
||||
content. When this happens the comment will be successfully created on Reddit
|
||||
and can be retried by drawing the comment from the user's comment history.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.reply("reply")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.reply("reply")
|
||||
|
||||
"""
|
||||
data = {"text": body, "thing_id": self.fullname}
|
||||
comments = self._reddit.post(API_PATH["comment"], data=data)
|
||||
try:
|
||||
return comments[0]
|
||||
except IndexError:
|
||||
return None
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Provide the ReportableMixin class."""
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class ReportableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be reported."""
|
||||
|
||||
def report(self, reason: str):
|
||||
"""Report this object to the moderators of its subreddit.
|
||||
|
||||
:param reason: The reason for reporting.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if ``reason`` is longer than 100
|
||||
characters.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.report("report reason")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.report("report reason")
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["report"], data={"id": self.fullname, "reason": reason}
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Provide the SavableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ....const import API_PATH
|
||||
from ....util import _deprecate_args
|
||||
|
||||
|
||||
class SavableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be saved."""
|
||||
|
||||
@_deprecate_args("category")
|
||||
def save(self, *, category: str | None = None):
|
||||
"""Save the object.
|
||||
|
||||
:param category: The category to save to. If the authenticated user does not
|
||||
have Reddit Premium this value is ignored by Reddit (default: ``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.save(category="view later")
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.save()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unsave`
|
||||
|
||||
"""
|
||||
self._reddit.post(
|
||||
API_PATH["save"], data={"category": category, "id": self.fullname}
|
||||
)
|
||||
|
||||
def unsave(self):
|
||||
"""Unsave the object.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.unsave()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.unsave()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.save`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["unsave"], data={"id": self.fullname})
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Provide the VotableMixin class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ....const import API_PATH
|
||||
|
||||
|
||||
class VotableMixin:
|
||||
"""Interface for :class:`.RedditBase` classes that can be voted on."""
|
||||
|
||||
def _vote(self, direction: int):
|
||||
self._reddit.post(
|
||||
API_PATH["vote"], data={"dir": str(direction), "id": self.fullname}
|
||||
)
|
||||
|
||||
def clear_vote(self):
|
||||
"""Clear the authenticated user's vote on the object.
|
||||
|
||||
.. note::
|
||||
|
||||
Votes must be cast by humans. That is, API clients proxying a human's action
|
||||
one-for-one are OK, but bots deciding how to vote on content or amplifying a
|
||||
human's vote are not. See the reddit rules for more details on what
|
||||
constitutes vote manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.clear_vote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.clear_vote()
|
||||
|
||||
"""
|
||||
self._vote(direction=0)
|
||||
|
||||
def downvote(self):
|
||||
"""Downvote the object.
|
||||
|
||||
.. note::
|
||||
|
||||
Votes must be cast by humans. That is, API clients proxying a human's action
|
||||
one-for-one are OK, but bots deciding how to vote on content or amplifying a
|
||||
human's vote are not. See the reddit rules for more details on what
|
||||
constitutes vote manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.downvote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.downvote()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.upvote`
|
||||
|
||||
"""
|
||||
self._vote(direction=-1)
|
||||
|
||||
def upvote(self):
|
||||
"""Upvote the object.
|
||||
|
||||
.. note::
|
||||
|
||||
Votes must be cast by humans. That is, API clients proxying a human's action
|
||||
one-for-one are OK, but bots deciding how to vote on content or amplifying a
|
||||
human's vote are not. See the reddit rules for more details on what
|
||||
constitutes vote manipulation. [`Ref
|
||||
<https://www.reddit.com/dev/api#POST_api_vote>`_]
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.upvote()
|
||||
|
||||
comment = reddit.comment("dxolpyc")
|
||||
comment.upvote()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.downvote`
|
||||
|
||||
"""
|
||||
self._vote(direction=1)
|
||||
344
backend/venv/Lib/site-packages/praw/models/reddit/modmail.py
Normal file
344
backend/venv/Lib/site-packages/praw/models/reddit/modmail.py
Normal file
@@ -0,0 +1,344 @@
|
||||
"""Provide models for new modmail."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args, snake_case_keys
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class ModmailObject(RedditBase):
|
||||
"""A base class for objects within a modmail conversation."""
|
||||
|
||||
AUTHOR_ATTRIBUTE = "author"
|
||||
STR_FIELD = "id"
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify the AUTHOR_ATTRIBUTE attribute."""
|
||||
if attribute == self.AUTHOR_ATTRIBUTE:
|
||||
value = self._reddit._objector.objectify(value)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
|
||||
class ModmailConversation(RedditBase):
|
||||
"""A class for modmail conversations.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== ===============================================================
|
||||
Attribute Description
|
||||
==================== ===============================================================
|
||||
``authors`` Provides an ordered list of :class:`.Redditor` instances. The
|
||||
authors of each message in the modmail conversation.
|
||||
``id`` The ID of the :class:`.ModmailConversation`.
|
||||
``is_highlighted`` Whether or not the :class:`.ModmailConversation` is
|
||||
highlighted.
|
||||
``is_internal`` Whether or not the :class:`.ModmailConversation` is a private
|
||||
mod conversation.
|
||||
``last_mod_update`` Time of the last mod message reply, represented in the `ISO
|
||||
8601`_ standard with timezone.
|
||||
``last_updated`` Time of the last message reply, represented in the `ISO 8601`_
|
||||
standard with timezone.
|
||||
``last_user_update`` Time of the last user message reply, represented in the `ISO
|
||||
8601`_ standard with timezone.
|
||||
``num_messages`` The number of messages in the :class:`.ModmailConversation`.
|
||||
``obj_ids`` Provides a list of dictionaries representing mod actions on the
|
||||
:class:`.ModmailConversation`. Each dict contains attributes of
|
||||
``"key"`` and ``"id"``. The key can be either ``""messages"``
|
||||
or ``"ModAction"``. ``"ModAction"`` represents
|
||||
archiving/highlighting etc.
|
||||
``owner`` Provides an instance of :class:`.Subreddit`. The subreddit that
|
||||
the :class:`.ModmailConversation` belongs to.
|
||||
``participant`` Provides an instance of :class:`.Redditor`. The participating
|
||||
user in the :class:`.ModmailConversation`.
|
||||
``subject`` The subject of the :class:`.ModmailConversation`.
|
||||
==================== ===============================================================
|
||||
|
||||
.. _iso 8601: https://en.wikipedia.org/wiki/ISO_8601
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def _convert_conversation_objects(data: dict[str, Any], reddit: praw.Reddit):
|
||||
"""Convert messages and mod actions to PRAW objects."""
|
||||
result = {"messages": [], "modActions": []}
|
||||
for thing in data["objIds"]:
|
||||
key = thing["key"]
|
||||
thing_data = data[key][thing["id"]]
|
||||
result[key].append(reddit._objector.objectify(thing_data))
|
||||
data.update(result)
|
||||
|
||||
@staticmethod
|
||||
def _convert_user_summary(data: dict[str, Any], reddit: praw.Reddit):
|
||||
"""Convert dictionaries of recent user history to PRAW objects."""
|
||||
parsers = {
|
||||
"recentComments": reddit._objector.parsers[reddit.config.kinds["comment"]],
|
||||
"recentConvos": ModmailConversation,
|
||||
"recentPosts": reddit._objector.parsers[reddit.config.kinds["submission"]],
|
||||
}
|
||||
for kind, parser in parsers.items():
|
||||
objects = []
|
||||
for thing_id, summary in data[kind].items():
|
||||
thing = parser(reddit, id=thing_id.rsplit("_", 1)[-1])
|
||||
if parser is not ModmailConversation:
|
||||
del summary["permalink"]
|
||||
for key, value in summary.items():
|
||||
setattr(thing, key, value)
|
||||
objects.append(thing)
|
||||
# Sort by id, oldest to newest
|
||||
data[kind] = sorted(objects, key=lambda x: int(x.id, base=36), reverse=True)
|
||||
|
||||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
data: dict[str, Any],
|
||||
reddit: praw.Reddit,
|
||||
) -> ModmailConversation:
|
||||
"""Return an instance of :class:`.ModmailConversation` from ``data``.
|
||||
|
||||
:param data: The structured data.
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
data["authors"] = [
|
||||
reddit._objector.objectify(author) for author in data["authors"]
|
||||
]
|
||||
for entity in "owner", "participant":
|
||||
data[entity] = reddit._objector.objectify(data[entity])
|
||||
|
||||
if data.get("user"):
|
||||
cls._convert_user_summary(data["user"], reddit)
|
||||
data["user"] = reddit._objector.objectify(data["user"])
|
||||
|
||||
data = snake_case_keys(data)
|
||||
|
||||
return cls(reddit, _data=data)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
mark_read: bool = False,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.ModmailConversation` instance.
|
||||
|
||||
:param mark_read: If ``True``, conversation is marked as read (default:
|
||||
``False``).
|
||||
|
||||
"""
|
||||
if bool(id) == bool(_data):
|
||||
msg = "Either 'id' or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
|
||||
if id:
|
||||
self.id = id
|
||||
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
self._info_params = {"markRead": True} if mark_read else None
|
||||
|
||||
def _build_conversation_list(
|
||||
self, other_conversations: list[ModmailConversation]
|
||||
) -> str:
|
||||
"""Return a comma-separated list of conversation IDs."""
|
||||
conversations = [self] + (other_conversations or [])
|
||||
return ",".join(conversation.id for conversation in conversations)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
other = self._reddit._objector.objectify(data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return "modmail_conversation", {"id": self.id}, self._info_params
|
||||
|
||||
def archive(self):
|
||||
"""Archive the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").archive()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_archive"].format(id=self.id))
|
||||
|
||||
def highlight(self):
|
||||
"""Highlight the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").highlight()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_highlight"].format(id=self.id))
|
||||
|
||||
@_deprecate_args("num_days")
|
||||
def mute(self, *, num_days: int = 3):
|
||||
"""Mute the non-mod user associated with the conversation.
|
||||
|
||||
:param num_days: Duration of mute in days. Valid options are ``3``, ``7``, or
|
||||
``28`` (default: ``3``).
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").mute()
|
||||
|
||||
To mute for 7 days:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").mute(num_days=7)
|
||||
|
||||
"""
|
||||
params = {"num_hours": num_days * 24} if num_days != 3 else {}
|
||||
self._reddit.request(
|
||||
method="POST",
|
||||
params=params,
|
||||
path=API_PATH["modmail_mute"].format(id=self.id),
|
||||
)
|
||||
|
||||
@_deprecate_args("other_conversations")
|
||||
def read(self, *, other_conversations: list[ModmailConversation] | None = None):
|
||||
"""Mark the conversation(s) as read.
|
||||
|
||||
:param other_conversations: A list of other conversations to mark (default:
|
||||
``None``).
|
||||
|
||||
For example, to mark the conversation as read along with other recent
|
||||
conversations from the same user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
conversation = subreddit.modmail.conversation("2gmz")
|
||||
conversation.read(other_conversations=conversation.user.recent_convos)
|
||||
|
||||
"""
|
||||
data = {"conversationIds": self._build_conversation_list(other_conversations)}
|
||||
self._reddit.post(API_PATH["modmail_read"], data=data)
|
||||
|
||||
@_deprecate_args("body", "author_hidden", "internal")
|
||||
def reply(
|
||||
self, *, author_hidden: bool = False, body: str, internal: bool = False
|
||||
) -> ModmailMessage:
|
||||
"""Reply to the conversation.
|
||||
|
||||
:param author_hidden: When ``True``, author is hidden from non-moderators
|
||||
(default: ``False``).
|
||||
:param body: The Markdown formatted content for a message.
|
||||
:param internal: When ``True``, message is a private moderator note, hidden from
|
||||
non-moderators (default: ``False``).
|
||||
|
||||
:returns: A :class:`.ModmailMessage` object for the newly created message.
|
||||
|
||||
For example, to reply to the non-mod user while hiding your username:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
conversation = reddit.subreddit("test").modmail("2gmz")
|
||||
conversation.reply(body="Message body", author_hidden=True)
|
||||
|
||||
To create a private moderator note on the conversation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
conversation.reply(body="Message body", internal=True)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"body": body,
|
||||
"isAuthorHidden": author_hidden,
|
||||
"isInternal": internal,
|
||||
}
|
||||
response = self._reddit.post(
|
||||
API_PATH["modmail_conversation"].format(id=self.id), data=data
|
||||
)
|
||||
if isinstance(response, dict):
|
||||
# Reddit recently changed the response format, so we need to handle both in case they change it back
|
||||
message_id = response["conversation"]["objIds"][-1]["id"]
|
||||
message_data = response["messages"][message_id]
|
||||
return self._reddit._objector.objectify(message_data)
|
||||
for message in response.messages: # noqa: RET503
|
||||
if message.id == response.obj_ids[-1]["id"]:
|
||||
return message
|
||||
|
||||
def unarchive(self):
|
||||
"""Unarchive the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unarchive()
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["modmail_unarchive"].format(id=self.id))
|
||||
|
||||
def unhighlight(self):
|
||||
"""Un-highlight the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unhighlight()
|
||||
|
||||
"""
|
||||
self._reddit.delete(API_PATH["modmail_highlight"].format(id=self.id))
|
||||
|
||||
def unmute(self):
|
||||
"""Unmute the non-mod user associated with the conversation.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").modmail("2gmz").unmute()
|
||||
|
||||
"""
|
||||
self._reddit.request(
|
||||
method="POST", path=API_PATH["modmail_unmute"].format(id=self.id)
|
||||
)
|
||||
|
||||
@_deprecate_args("other_conversations")
|
||||
def unread(self, *, other_conversations: list[ModmailConversation] | None = None):
|
||||
"""Mark the conversation(s) as unread.
|
||||
|
||||
:param other_conversations: A list of other conversations to mark (default:
|
||||
``None``).
|
||||
|
||||
For example, to mark the conversation as unread along with other recent
|
||||
conversations from the same user:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
conversation = subreddit.modmail.conversation("2gmz")
|
||||
conversation.unread(other_conversations=conversation.user.recent_convos)
|
||||
|
||||
"""
|
||||
data = {"conversationIds": self._build_conversation_list(other_conversations)}
|
||||
self._reddit.post(API_PATH["modmail_unread"], data=data)
|
||||
|
||||
|
||||
class ModmailAction(ModmailObject):
|
||||
"""A class for moderator actions on modmail conversations."""
|
||||
|
||||
|
||||
class ModmailMessage(ModmailObject):
|
||||
"""A class for modmail messages."""
|
||||
83
backend/venv/Lib/site-packages/praw/models/reddit/more.py
Normal file
83
backend/venv/Lib/site-packages/praw/models/reddit/more.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Provide the MoreComments class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ..base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class MoreComments(PRAWBase):
|
||||
"""A class indicating there are more comments."""
|
||||
|
||||
def __eq__(self, other: str | MoreComments) -> bool:
|
||||
"""Return ``True`` if these :class:`.MoreComments` instances are the same."""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.count == other.count and self.children == other.children
|
||||
return super().__eq__(other)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.MoreComments` instance."""
|
||||
self.count = self.parent_id = None
|
||||
self.children = []
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._comments = None
|
||||
self.submission = None
|
||||
|
||||
def __lt__(self, other: MoreComments) -> bool:
|
||||
"""Provide a sort order on the :class:`.MoreComments` object."""
|
||||
# To work with heapq a "smaller" item is the one with the most comments. We are
|
||||
# intentionally making the biggest element the smallest element to turn the
|
||||
# min-heap implementation in heapq into a max-heap.
|
||||
return self.count > other.count
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
children = self.children[:4]
|
||||
if len(self.children) > 4:
|
||||
children[-1] = "..."
|
||||
return f"<{self.__class__.__name__} count={self.count}, children={children!r}>"
|
||||
|
||||
def _continue_comments(self, update: bool):
|
||||
assert not self.children, "Please file a bug report with PRAW."
|
||||
parent = self._load_comment(self.parent_id.split("_", 1)[1])
|
||||
self._comments = parent.replies
|
||||
if update:
|
||||
for comment in self._comments:
|
||||
comment.submission = self.submission
|
||||
return self._comments
|
||||
|
||||
def _load_comment(self, comment_id: str):
|
||||
path = f"{API_PATH['submission'].format(id=self.submission.id)}_/{comment_id}"
|
||||
_, comments = self._reddit.get(
|
||||
path,
|
||||
params={
|
||||
"limit": self.submission.comment_limit,
|
||||
"sort": self.submission.comment_sort,
|
||||
},
|
||||
)
|
||||
assert len(comments.children) == 1, "Please file a bug report with PRAW."
|
||||
return comments.children[0]
|
||||
|
||||
@_deprecate_args("update")
|
||||
def comments(self, *, update: bool = True) -> list[praw.models.Comment]:
|
||||
"""Fetch and return the comments for a single :class:`.MoreComments` object."""
|
||||
if self._comments is None:
|
||||
if self.count == 0: # Handle "continue this thread"
|
||||
return self._continue_comments(update)
|
||||
assert self.children, "Please file a bug report with PRAW."
|
||||
data = {
|
||||
"children": ",".join(self.children),
|
||||
"link_id": self.submission.fullname,
|
||||
"sort": self.submission.comment_sort,
|
||||
}
|
||||
self._comments = self._reddit.post(API_PATH["morechildren"], data=data)
|
||||
if update:
|
||||
for comment in self._comments:
|
||||
comment.submission = self.submission
|
||||
return self._comments
|
||||
240
backend/venv/Lib/site-packages/praw/models/reddit/multi.py
Normal file
240
backend/venv/Lib/site-packages/praw/models/reddit/multi.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Provide the Multireddit class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from ..listing.mixins import SubredditListingMixin
|
||||
from .base import RedditBase
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit, SubredditStream
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Multireddit(SubredditListingMixin, RedditBase):
|
||||
r"""A class for users' multireddits.
|
||||
|
||||
This is referred to as a "Custom Feed" on the Reddit UI.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== ==============================================================
|
||||
Attribute Description
|
||||
==================== ==============================================================
|
||||
``can_edit`` A ``bool`` representing whether or not the authenticated user
|
||||
may edit the multireddit.
|
||||
``copied_from`` The multireddit that the multireddit was copied from, if it
|
||||
exists, otherwise ``None``.
|
||||
``created_utc`` When the multireddit was created, in `Unix Time`_.
|
||||
``description_html`` The description of the multireddit, as HTML.
|
||||
``description_md`` The description of the multireddit, as Markdown.
|
||||
``display_name`` The display name of the multireddit.
|
||||
``name`` The name of the multireddit.
|
||||
``over_18`` A ``bool`` representing whether or not the multireddit is
|
||||
restricted for users over 18.
|
||||
``subreddits`` A list of :class:`.Subreddit`\ s that make up the multireddit.
|
||||
``visibility`` The visibility of the multireddit, either ``"private"``,
|
||||
``"public"``, or ``"hidden"``.
|
||||
==================== ==============================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "path"
|
||||
RE_INVALID = re.compile(r"[\W_]+", re.UNICODE)
|
||||
|
||||
@staticmethod
|
||||
def sluggify(title: str) -> str:
|
||||
"""Return a slug version of the title.
|
||||
|
||||
:param title: The title to make a slug of.
|
||||
|
||||
Adapted from Reddit's utils.py.
|
||||
|
||||
"""
|
||||
title = Multireddit.RE_INVALID.sub("_", title).strip("_").lower()
|
||||
if len(title) > 21: # truncate to nearest word
|
||||
title = title[:21]
|
||||
last_word = title.rfind("_")
|
||||
if last_word > 0:
|
||||
title = title[:last_word]
|
||||
return title or "_"
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> SubredditStream:
|
||||
"""Provide an instance of :class:`.SubredditStream`.
|
||||
|
||||
Streams can be used to indefinitely retrieve new comments made to a multireddit,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.multireddit(redditor="spez", name="fun").stream.comments():
|
||||
print(comment)
|
||||
|
||||
Additionally, new submissions can be retrieved via the stream. In the following
|
||||
example all new submissions to the multireddit are fetched:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.multireddit(
|
||||
redditor="bboe", name="games"
|
||||
).stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return SubredditStream(self)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.Multireddit` instance."""
|
||||
self.path = None
|
||||
super().__init__(reddit, _data=_data)
|
||||
self._author = Redditor(reddit, self.path.split("/", 3)[2])
|
||||
self._path = API_PATH["multireddit"].format(multi=self.name, user=self._author)
|
||||
self.path = f"/{self._path[:-1]}" # Prevent requests for path
|
||||
if "subreddits" in self.__dict__:
|
||||
self.subreddits = [Subreddit(reddit, x["name"]) for x in self.subreddits]
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"multireddit_api",
|
||||
{"multi": self.name, "user": self._author.name},
|
||||
None,
|
||||
)
|
||||
|
||||
def add(self, subreddit: praw.models.Subreddit):
|
||||
"""Add a subreddit to this multireddit.
|
||||
|
||||
:param subreddit: The subreddit to add to this multi.
|
||||
|
||||
For example, to add r/test to multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
reddit.multireddit(redditor="bboe", name="test").add(subreddit)
|
||||
|
||||
"""
|
||||
url = API_PATH["multireddit_update"].format(
|
||||
multi=self.name, user=self._author, subreddit=subreddit
|
||||
)
|
||||
self._reddit.put(url, data={"model": dumps({"name": str(subreddit)})})
|
||||
self._reset_attributes("subreddits")
|
||||
|
||||
@_deprecate_args("display_name")
|
||||
def copy(self, *, display_name: str | None = None) -> praw.models.Multireddit:
|
||||
"""Copy this multireddit and return the new multireddit.
|
||||
|
||||
:param display_name: The display name for the copied multireddit. Reddit will
|
||||
generate the ``name`` field from this display name. When not provided the
|
||||
copy will use the same display name and name as this multireddit.
|
||||
|
||||
To copy the multireddit ``bboe/test`` with a name of ``"testing"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").copy(display_name="testing")
|
||||
|
||||
"""
|
||||
if display_name:
|
||||
name = self.sluggify(display_name)
|
||||
else:
|
||||
display_name = self.display_name
|
||||
name = self.name
|
||||
data = {
|
||||
"display_name": display_name,
|
||||
"from": self.path,
|
||||
"to": API_PATH["multireddit"].format(
|
||||
multi=name, user=self._reddit.user.me()
|
||||
),
|
||||
}
|
||||
return self._reddit.post(API_PATH["multireddit_copy"], data=data)
|
||||
|
||||
def delete(self):
|
||||
"""Delete this multireddit.
|
||||
|
||||
For example, to delete multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").delete()
|
||||
|
||||
"""
|
||||
path = API_PATH["multireddit_api"].format(
|
||||
multi=self.name, user=self._author.name
|
||||
)
|
||||
self._reddit.delete(path)
|
||||
|
||||
def remove(self, subreddit: praw.models.Subreddit):
|
||||
"""Remove a subreddit from this multireddit.
|
||||
|
||||
:param subreddit: The subreddit to remove from this multi.
|
||||
|
||||
For example, to remove r/test from multireddit ``bboe/test``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
reddit.multireddit(redditor="bboe", name="test").remove(subreddit)
|
||||
|
||||
"""
|
||||
url = API_PATH["multireddit_update"].format(
|
||||
multi=self.name, user=self._author, subreddit=subreddit
|
||||
)
|
||||
self._reddit.delete(url, data={"model": dumps({"name": str(subreddit)})})
|
||||
self._reset_attributes("subreddits")
|
||||
|
||||
def update(
|
||||
self,
|
||||
**updated_settings: str | list[str | praw.models.Subreddit | dict[str, str]],
|
||||
):
|
||||
"""Update this multireddit.
|
||||
|
||||
Keyword arguments are passed for settings that should be updated. They can any
|
||||
of:
|
||||
|
||||
:param display_name: The display name for this multireddit. Must be no longer
|
||||
than 50 characters.
|
||||
:param subreddits: Subreddits for this multireddit.
|
||||
:param description_md: Description for this multireddit, formatted in Markdown.
|
||||
:param icon_name: Can be one of: ``"art and design"``, ``"ask"``, ``"books"``,
|
||||
``"business"``, ``"cars"``, ``"comics"``, ``"cute animals"``, ``"diy"``,
|
||||
``"entertainment"``, ``"food and drink"``, ``"funny"``, ``"games"``,
|
||||
``"grooming"``, ``"health"``, ``"life advice"``, ``"military"``, ``"models
|
||||
pinup"``, ``"music"``, ``"news"``, ``"philosophy"``, ``"pictures and
|
||||
gifs"``, ``"science"``, ``"shopping"``, ``"sports"``, ``"style"``,
|
||||
``"tech"``, ``"travel"``, ``"unusual stories"``, ``"video"``, or ``None``.
|
||||
:param key_color: RGB hex color code of the form ``"#FFFFFF"``.
|
||||
:param visibility: Can be one of: ``"hidden"``, ``"private"``, or ``"public"``.
|
||||
:param weighting_scheme: Can be one of: ``"classic"`` or ``"fresh"``.
|
||||
|
||||
For example, to rename multireddit ``"bboe/test"`` to ``"bboe/testing"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.multireddit(redditor="bboe", name="test").update(display_name="testing")
|
||||
|
||||
"""
|
||||
if "subreddits" in updated_settings:
|
||||
updated_settings["subreddits"] = [
|
||||
{"name": str(sub)} for sub in updated_settings["subreddits"]
|
||||
]
|
||||
path = API_PATH["multireddit_api"].format(
|
||||
multi=self.name, user=self._author.name
|
||||
)
|
||||
new = self._reddit.put(path, data={"model": dumps(updated_settings)})
|
||||
self.__dict__.update(new.__dict__)
|
||||
112
backend/venv/Lib/site-packages/praw/models/reddit/poll.py
Normal file
112
backend/venv/Lib/site-packages/praw/models/reddit/poll.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Provide poll-related classes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ...util import cachedproperty
|
||||
from ..base import PRAWBase
|
||||
|
||||
|
||||
class PollOption(PRAWBase):
|
||||
"""Class to represent one option of a poll.
|
||||
|
||||
If ``submission`` is a poll :class:`.Submission`, access the poll's options like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
poll_data = submission.poll_data
|
||||
|
||||
# By index -- print the first option
|
||||
print(poll_data.options[0])
|
||||
|
||||
# By ID -- print the option with ID "576797"
|
||||
print(poll_data.option("576797"))
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
============== =================================================
|
||||
Attribute Description
|
||||
============== =================================================
|
||||
``id`` ID of the poll option.
|
||||
``text`` The text of the poll option.
|
||||
``vote_count`` The number of votes the poll option has received.
|
||||
============== =================================================
|
||||
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"PollOption(id={self.id!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string version of the PollData, its text."""
|
||||
return self.text
|
||||
|
||||
|
||||
class PollData(PRAWBase):
|
||||
"""Class to represent poll data on a poll submission.
|
||||
|
||||
If ``submission`` is a poll :class:`.Submission`, access the poll data like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
poll_data = submission.poll_data
|
||||
print(f"There are {poll_data.total_vote_count} votes total.")
|
||||
print("The options are:")
|
||||
for option in poll_data.options:
|
||||
print(f"{option} ({option.vote_count} votes)")
|
||||
print(f"I voted for {poll_data.user_selection}.")
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
======================== =========================================================
|
||||
Attribute Description
|
||||
======================== =========================================================
|
||||
``options`` A list of :class:`.PollOption` of the poll.
|
||||
``total_vote_count`` The total number of votes cast in the poll.
|
||||
``user_selection`` The poll option selected by the authenticated user
|
||||
(possibly ``None``).
|
||||
``voting_end_timestamp`` Time the poll voting closes, represented in `Unix Time`_.
|
||||
======================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
@cachedproperty
|
||||
def user_selection(self) -> PollOption | None:
|
||||
"""Get the user's selection in this poll, if any.
|
||||
|
||||
:returns: The user's selection as a :class:`.PollOption`, or ``None`` if there
|
||||
is no choice.
|
||||
|
||||
"""
|
||||
if self._user_selection is None:
|
||||
return None
|
||||
return self.option(self._user_selection)
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify the options attribute, and save user_selection."""
|
||||
if attribute == "options" and isinstance(value, list):
|
||||
value = [PollOption(self._reddit, option) for option in value]
|
||||
elif attribute == "user_selection":
|
||||
attribute = "_user_selection"
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def option(self, option_id: str) -> PollOption:
|
||||
"""Get the option with the specified ID.
|
||||
|
||||
:param option_id: The ID of a poll option, as a ``str``.
|
||||
|
||||
:returns: The specified :class:`.PollOption`.
|
||||
|
||||
:raises: :py:class:`KeyError` if no option exists with the specified ID.
|
||||
|
||||
"""
|
||||
for option in self.options:
|
||||
if option.id == option_id:
|
||||
return option
|
||||
|
||||
msg = f"No poll option with ID {option_id!r}."
|
||||
raise KeyError(msg)
|
||||
494
backend/venv/Lib/site-packages/praw/models/reddit/redditor.py
Normal file
494
backend/venv/Lib/site-packages/praw/models/reddit/redditor.py
Normal file
@@ -0,0 +1,494 @@
|
||||
"""Provide the Redditor class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..listing.mixins import RedditorListingMixin
|
||||
from ..util import stream_generator
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, MessageableMixin
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Redditor(MessageableMixin, RedditorListingMixin, FullnameMixin, RedditBase):
|
||||
"""A class representing the users of Reddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
.. note::
|
||||
|
||||
Shadowbanned accounts are treated the same as non-existent accounts, meaning
|
||||
that they will not have any attributes.
|
||||
|
||||
.. note::
|
||||
|
||||
Suspended/banned accounts will only return the ``name`` and ``is_suspended``
|
||||
attributes.
|
||||
|
||||
=================================== ================================================
|
||||
Attribute Description
|
||||
=================================== ================================================
|
||||
``comment_karma`` The comment karma for the :class:`.Redditor`.
|
||||
``comments`` Provide an instance of :class:`.SubListing` for
|
||||
comment access.
|
||||
``submissions`` Provide an instance of :class:`.SubListing` for
|
||||
submission access.
|
||||
``created_utc`` Time the account was created, represented in
|
||||
`Unix Time`_.
|
||||
``has_verified_email`` Whether or not the :class:`.Redditor` has
|
||||
verified their email.
|
||||
``icon_img`` The url of the Redditors' avatar.
|
||||
``id`` The ID of the :class:`.Redditor`.
|
||||
``is_employee`` Whether or not the :class:`.Redditor` is a
|
||||
Reddit employee.
|
||||
``is_friend`` Whether or not the :class:`.Redditor` is friends
|
||||
with the authenticated user.
|
||||
``is_mod`` Whether or not the :class:`.Redditor` mods any
|
||||
subreddits.
|
||||
``is_gold`` Whether or not the :class:`.Redditor` has active
|
||||
Reddit Premium status.
|
||||
``is_suspended`` Whether or not the :class:`.Redditor` is
|
||||
currently suspended.
|
||||
``link_karma`` The link karma for the :class:`.Redditor`.
|
||||
``name`` The Redditor's username.
|
||||
``subreddit`` If the :class:`.Redditor` has created a
|
||||
user-subreddit, provides a dictionary of
|
||||
additional attributes. See below.
|
||||
``subreddit["banner_img"]`` The URL of the user-subreddit banner.
|
||||
``subreddit["name"]`` The fullname of the user-subreddit.
|
||||
``subreddit["over_18"]`` Whether or not the user-subreddit is NSFW.
|
||||
``subreddit["public_description"]`` The public description of the user-subreddit.
|
||||
``subreddit["subscribers"]`` The number of users subscribed to the
|
||||
user-subreddit.
|
||||
``subreddit["title"]`` The title of the user-subreddit.
|
||||
=================================== ================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "name"
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, reddit: praw.Reddit, data: dict[str, Any]) -> Redditor | None:
|
||||
"""Return an instance of :class:`.Redditor`, or ``None`` from ``data``."""
|
||||
if data == "[deleted]":
|
||||
return None
|
||||
return cls(reddit, data)
|
||||
|
||||
@cachedproperty
|
||||
def notes(self) -> praw.models.RedditorModNotes:
|
||||
"""Provide an instance of :class:`.RedditorModNotes`.
|
||||
|
||||
This provides an interface for managing moderator notes for a redditor.
|
||||
|
||||
.. note::
|
||||
|
||||
The authenticated user must be a moderator of the provided subreddit(s).
|
||||
|
||||
For example, all the notes for u/spez in r/test can be iterated through like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
redditor = reddit.redditor("spez")
|
||||
|
||||
for note in redditor.notes.subreddits("test"):
|
||||
print(f"{note.label}: {note.note}")
|
||||
|
||||
"""
|
||||
from praw.models.mod_notes import RedditorModNotes
|
||||
|
||||
return RedditorModNotes(self._reddit, self)
|
||||
|
||||
@cachedproperty
|
||||
def stream(self) -> praw.models.reddit.redditor.RedditorStream:
|
||||
"""Provide an instance of :class:`.RedditorStream`.
|
||||
|
||||
Streams can be used to indefinitely retrieve new comments made by a redditor,
|
||||
like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.redditor("spez").stream.comments():
|
||||
print(comment)
|
||||
|
||||
Additionally, new submissions can be retrieved via the stream. In the following
|
||||
example all submissions are fetched via the redditor u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.redditor("spez").stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return RedditorStream(self)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["redditor"]
|
||||
|
||||
@property
|
||||
def _path(self) -> str:
|
||||
return API_PATH["user"].format(user=self)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
name: str | None = None,
|
||||
fullname: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Redditor` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param name: The name of the redditor.
|
||||
:param fullname: The fullname of the redditor, starting with ``t2_``.
|
||||
|
||||
Exactly one of ``name``, ``fullname`` or ``_data`` must be provided.
|
||||
|
||||
"""
|
||||
if (name, fullname, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'name', 'fullname', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
if _data:
|
||||
assert ( # noqa: PT018
|
||||
isinstance(_data, dict) and "name" in _data
|
||||
), "Please file a bug with PRAW."
|
||||
self._listing_use_sort = True
|
||||
if name:
|
||||
self.name = name
|
||||
elif fullname:
|
||||
self._fullname = fullname
|
||||
super().__init__(reddit, _data=_data, _extra_attribute_to_check="_fullname")
|
||||
|
||||
def __setattr__(self, name: str, value: Any):
|
||||
"""Objectify the subreddit attribute."""
|
||||
if name == "subreddit" and value:
|
||||
from .user_subreddit import UserSubreddit
|
||||
|
||||
value = UserSubreddit(reddit=self._reddit, _data=value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
other = type(self)(self._reddit, _data=data)
|
||||
self.__dict__.update(other.__dict__)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
if hasattr(self, "_fullname"):
|
||||
self.name = self._fetch_username(self._fullname)
|
||||
return "user_about", {"user": self.name}, None
|
||||
|
||||
def _fetch_username(self, fullname: str):
|
||||
return self._reddit.get(API_PATH["user_by_fullname"], params={"ids": fullname})[
|
||||
fullname
|
||||
]["name"]
|
||||
|
||||
def _friend(self, *, data: dict[str, Any], method: str):
|
||||
url = API_PATH["friend_v1"].format(user=self)
|
||||
self._reddit.request(data=dumps(data), method=method, path=url)
|
||||
|
||||
def block(self):
|
||||
"""Block the :class:`.Redditor`.
|
||||
|
||||
For example, to block :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").block()
|
||||
|
||||
.. note::
|
||||
|
||||
Blocking a trusted user will remove that user from your trusted list.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.trust`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["block_user"], params={"name": self.name})
|
||||
|
||||
def distrust(self):
|
||||
"""Remove the :class:`.Redditor` from your whitelist of trusted users.
|
||||
|
||||
For example, to remove :class:`.Redditor` u/spez from your whitelist:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").distrust()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.trust`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["remove_whitelisted"], data={"name": self.name})
|
||||
|
||||
@_deprecate_args("note")
|
||||
def friend(self, *, note: str = None):
|
||||
"""Friend the :class:`.Redditor`.
|
||||
|
||||
:param note: A note to save along with the relationship. Requires Reddit Premium
|
||||
(default: ``None``).
|
||||
|
||||
Calling this method subsequent times will update the note.
|
||||
|
||||
For example, to friend u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").friend()
|
||||
|
||||
To add a note to the friendship (requires Reddit Premium):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").friend(note="My favorite admin")
|
||||
|
||||
"""
|
||||
self._friend(data={"note": note} if note else {}, method="PUT")
|
||||
|
||||
def friend_info(self) -> praw.models.Redditor:
|
||||
"""Return a :class:`.Redditor` instance with specific friend-related attributes.
|
||||
|
||||
:returns: A :class:`.Redditor` instance with fields ``date``, ``id``, and
|
||||
possibly ``note`` if the authenticated user has Reddit Premium.
|
||||
|
||||
For example, to get the friendship information of :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
info = reddit.redditor("spez").friend_info
|
||||
friend_data = info.date
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["friend_v1"].format(user=self))
|
||||
|
||||
@_deprecate_args("months")
|
||||
def gild(self, *, months: int = 1):
|
||||
"""Gild the :class:`.Redditor`.
|
||||
|
||||
:param months: Specifies the number of months to gild up to 36 (default: ``1``).
|
||||
|
||||
For example, to gild :class:`.Redditor` u/spez for 1 month:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").gild(months=1)
|
||||
|
||||
"""
|
||||
if months < 1 or months > 36:
|
||||
msg = "months must be between 1 and 36"
|
||||
raise TypeError(msg)
|
||||
self._reddit.post(
|
||||
API_PATH["gild_user"].format(username=self), data={"months": months}
|
||||
)
|
||||
|
||||
def moderated(self) -> list[praw.models.Subreddit]:
|
||||
"""Return a list of the redditor's moderated subreddits.
|
||||
|
||||
:returns: A list of :class:`.Subreddit` objects. Return ``[]`` if the redditor
|
||||
has no moderated subreddits.
|
||||
|
||||
:raises: ``prawcore.ServerError`` in certain circumstances. See the note below.
|
||||
|
||||
.. note::
|
||||
|
||||
The redditor's own user profile subreddit will not be returned, but other
|
||||
user profile subreddits they moderate will be returned.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for subreddit in reddit.redditor("spez").moderated():
|
||||
print(subreddit.display_name)
|
||||
print(subreddit.title)
|
||||
|
||||
.. note::
|
||||
|
||||
A ``prawcore.ServerError`` exception may be raised if the redditor moderates
|
||||
a large number of subreddits. If that happens, try switching to
|
||||
:ref:`read-only mode <read_only_application>`. For example,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.read_only = True
|
||||
for subreddit in reddit.redditor("reddit").moderated():
|
||||
print(str(subreddit))
|
||||
|
||||
It is possible that requests made in read-only mode will also raise a
|
||||
``prawcore.ServerError`` exception.
|
||||
|
||||
When used in read-only mode, this method does not retrieve information about
|
||||
subreddits that require certain special permissions to access, e.g., private
|
||||
subreddits and premium-only subreddits.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.User.moderator_subreddits`
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["moderated"].format(user=self)) or []
|
||||
|
||||
def multireddits(self) -> list[praw.models.Multireddit]:
|
||||
"""Return a list of the redditor's public multireddits.
|
||||
|
||||
For example, to to get :class:`.Redditor` u/spez's multireddits:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
multireddits = reddit.redditor("spez").multireddits()
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["multireddit_user"].format(user=self))
|
||||
|
||||
def trophies(self) -> list[praw.models.Trophy]:
|
||||
"""Return a list of the redditor's trophies.
|
||||
|
||||
:returns: A list of :class:`.Trophy` objects. Return ``[]`` if the redditor has
|
||||
no trophies.
|
||||
|
||||
:raises: :class:`.RedditAPIException` if the redditor doesn't exist.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for trophy in reddit.redditor("spez").trophies():
|
||||
print(trophy.name)
|
||||
print(trophy.description)
|
||||
|
||||
"""
|
||||
return list(self._reddit.get(API_PATH["trophies"].format(user=self)))
|
||||
|
||||
def trust(self):
|
||||
"""Add the :class:`.Redditor` to your whitelist of trusted users.
|
||||
|
||||
Trusted users will always be able to send you PMs.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("AaronSw").trust()
|
||||
|
||||
Use the ``accept_pms`` parameter of :meth:`.Preferences.update` to toggle your
|
||||
``accept_pms`` setting between ``"everyone"`` and ``"whitelisted"``. For
|
||||
example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Accept private messages from everyone:
|
||||
reddit.user.preferences.update(accept_pms="everyone")
|
||||
# Only accept private messages from trusted users:
|
||||
reddit.user.preferences.update(accept_pms="whitelisted")
|
||||
|
||||
You may trust a user even if your ``accept_pms`` setting is switched to
|
||||
``"everyone"``.
|
||||
|
||||
.. note::
|
||||
|
||||
You are allowed to have a user on your blocked list and your friends list at
|
||||
the same time. However, you cannot trust a user who is on your blocked list.
|
||||
|
||||
.. seealso::
|
||||
|
||||
- :meth:`.distrust`
|
||||
- :meth:`.Preferences.update`
|
||||
- :meth:`.trusted`
|
||||
|
||||
"""
|
||||
self._reddit.post(API_PATH["add_whitelisted"], data={"name": self.name})
|
||||
|
||||
def unblock(self):
|
||||
"""Unblock the :class:`.Redditor`.
|
||||
|
||||
For example, to unblock :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").unblock()
|
||||
|
||||
"""
|
||||
data = {
|
||||
"container": self._reddit.user.me().fullname,
|
||||
"name": str(self),
|
||||
"type": "enemy",
|
||||
}
|
||||
url = API_PATH["unfriend"].format(subreddit="all")
|
||||
self._reddit.post(url, data=data)
|
||||
|
||||
def unfriend(self):
|
||||
"""Unfriend the :class:`.Redditor`.
|
||||
|
||||
For example, to unfriend :class:`.Redditor` u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.redditor("spez").unfriend()
|
||||
|
||||
"""
|
||||
self._friend(data={"id": str(self)}, method="DELETE")
|
||||
|
||||
|
||||
class RedditorStream:
|
||||
"""Provides submission and comment streams."""
|
||||
|
||||
def __init__(self, redditor: praw.models.Redditor):
|
||||
"""Initialize a :class:`.RedditorStream` instance.
|
||||
|
||||
:param redditor: The redditor associated with the streams.
|
||||
|
||||
"""
|
||||
self.redditor = redditor
|
||||
|
||||
def comments(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Generator[praw.models.Comment, None, None]:
|
||||
"""Yield new comments as they become available.
|
||||
|
||||
Comments are yielded oldest first. Up to 100 historical comments will initially
|
||||
be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new comments made by redditor u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for comment in reddit.redditor("spez").stream.comments():
|
||||
print(comment)
|
||||
|
||||
"""
|
||||
return stream_generator(self.redditor.comments.new, **stream_options)
|
||||
|
||||
def submissions(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Generator[praw.models.Submission, None, None]:
|
||||
"""Yield new submissions as they become available.
|
||||
|
||||
Submissions are yielded oldest first. Up to 100 historical submissions will
|
||||
initially be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
For example, to retrieve all new submissions made by redditor u/spez, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.redditor("spez").stream.submissions():
|
||||
print(submission)
|
||||
|
||||
"""
|
||||
return stream_generator(self.redditor.submissions.new, **stream_options)
|
||||
@@ -0,0 +1,255 @@
|
||||
"""Provide the Removal Reason class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from warnings import warn
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class RemovalReason(RedditBase):
|
||||
"""An individual Removal Reason object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=========== ==================================
|
||||
Attribute Description
|
||||
=========== ==================================
|
||||
``id`` The ID of the removal reason.
|
||||
``message`` The message of the removal reason.
|
||||
``title`` The title of the removal reason.
|
||||
=========== ==================================
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def _warn_reason_id(
|
||||
*, id_value: str | None, reason_id_value: str | None
|
||||
) -> str | None:
|
||||
"""Reason ID param is deprecated. Warns if it's used.
|
||||
|
||||
:param id_value: Returns the actual value of parameter ``id`` is parameter
|
||||
``reason_id`` is not used.
|
||||
:param reason_id_value: The value passed as parameter ``reason_id``.
|
||||
|
||||
"""
|
||||
if reason_id_value is not None:
|
||||
warn(
|
||||
"Parameter 'reason_id' is deprecated. Either use positional arguments"
|
||||
' (e.g., reason_id="x" -> "x") or change the parameter name to \'id\''
|
||||
' (e.g., reason_id="x" -> id="x"). This parameter will be removed in'
|
||||
" PRAW 8.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
return reason_id_value
|
||||
return id_value
|
||||
|
||||
def __eq__(self, other: str | RemovalReason) -> bool:
|
||||
"""Return whether the other instance equals the current."""
|
||||
if isinstance(other, str):
|
||||
return other == str(self)
|
||||
return isinstance(other, self.__class__) and str(self) == str(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
"""Return the hash of the current instance."""
|
||||
return hash(self.__class__.__name__) ^ hash(str(self))
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
id: str | None = None,
|
||||
reason_id: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.RemovalReason` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param subreddit: An instance of :class:`.Subreddit`.
|
||||
:param id: The ID of the removal reason.
|
||||
:param reason_id: The original name of the ``id`` parameter. Used for backwards
|
||||
compatibility. This parameter should not be used.
|
||||
|
||||
"""
|
||||
reason_id = self._warn_reason_id(id_value=id, reason_id_value=reason_id)
|
||||
if (reason_id, _data).count(None) != 1:
|
||||
msg = "Either id or _data needs to be given."
|
||||
raise ValueError(msg)
|
||||
|
||||
if reason_id:
|
||||
self.id = reason_id
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for removal_reason in self.subreddit.mod.removal_reasons:
|
||||
if removal_reason.id == self.id:
|
||||
self.__dict__.update(removal_reason.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"Subreddit {self.subreddit} does not have the removal reason {self.id}"
|
||||
raise ClientException(msg)
|
||||
|
||||
def delete(self):
|
||||
"""Delete a removal reason from this subreddit.
|
||||
|
||||
To delete ``"141vv5c16py7d"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons["141vv5c16py7d"].delete()
|
||||
|
||||
"""
|
||||
url = API_PATH["removal_reason"].format(subreddit=self.subreddit, id=self.id)
|
||||
self._reddit.delete(url)
|
||||
|
||||
@_deprecate_args("message", "title")
|
||||
def update(self, *, message: str | None = None, title: str | None = None):
|
||||
"""Update the removal reason from this subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Existing values will be used for any unspecified arguments.
|
||||
|
||||
:param message: The removal reason's new message.
|
||||
:param title: The removal reason's new title.
|
||||
|
||||
To update ``"141vv5c16py7d"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons["141vv5c16py7d"].update(
|
||||
title="New title", message="New message"
|
||||
)
|
||||
|
||||
"""
|
||||
url = API_PATH["removal_reason"].format(subreddit=self.subreddit, id=self.id)
|
||||
data = {
|
||||
name: getattr(self, name) if value is None else value
|
||||
for name, value in {"message": message, "title": title}.items()
|
||||
}
|
||||
self._reddit.put(url, data=data)
|
||||
|
||||
|
||||
class SubredditRemovalReasons:
|
||||
"""Provide a set of functions to a :class:`.Subreddit`'s removal reasons."""
|
||||
|
||||
@cachedproperty
|
||||
def _removal_reason_list(self) -> list[RemovalReason]:
|
||||
"""Get a list of Removal Reason objects.
|
||||
|
||||
:returns: A list of instances of :class:`.RemovalReason`.
|
||||
|
||||
"""
|
||||
response = self._reddit.get(
|
||||
API_PATH["removal_reasons_list"].format(subreddit=self.subreddit)
|
||||
)
|
||||
return [
|
||||
RemovalReason(
|
||||
self._reddit, self.subreddit, _data=response["data"][reason_id]
|
||||
)
|
||||
for reason_id in response["order"]
|
||||
]
|
||||
|
||||
def __getitem__(self, reason_id: str | int | slice) -> RemovalReason:
|
||||
"""Return the Removal Reason with the ID/number/slice ``reason_id``.
|
||||
|
||||
:param reason_id: The ID or index of the removal reason
|
||||
|
||||
.. note::
|
||||
|
||||
Removal reasons fetched using a specific rule name are lazily loaded, so you
|
||||
might have to access an attribute to get all the expected attributes.
|
||||
|
||||
This method is to be used to fetch a specific removal reason, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reason_id = "141vv5c16py7d"
|
||||
reason = reddit.subreddit("test").mod.removal_reasons[reason_id]
|
||||
print(reason)
|
||||
|
||||
You can also use indices to get a numbered removal reason. Since Python uses
|
||||
0-indexing, the first removal reason is index 0, and so on.
|
||||
|
||||
.. note::
|
||||
|
||||
Both negative indices and slices can be used to interact with the removal
|
||||
reasons.
|
||||
|
||||
:raises: :py:class:`IndexError` if a removal reason of a specific number does
|
||||
not exist.
|
||||
|
||||
For example, to get the second removal reason of r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reason = reddit.subreddit("test").mod.removal_reasons[1]
|
||||
|
||||
To get the last three removal reasons in a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reasons = reddit.subreddit("test").mod.removal_reasons[-3:]
|
||||
for reason in reasons:
|
||||
print(reason)
|
||||
|
||||
"""
|
||||
if not isinstance(reason_id, str):
|
||||
return self._removal_reason_list[reason_id]
|
||||
return RemovalReason(self._reddit, self.subreddit, reason_id)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditRemovalReasons` instance.
|
||||
|
||||
:param subreddit: The subreddit whose removal reasons to work with.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> Iterator[RemovalReason]:
|
||||
"""Return a list of Removal Reasons for the subreddit.
|
||||
|
||||
This method is used to discover all removal reasons for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for removal_reason in reddit.subreddit("test").mod.removal_reasons:
|
||||
print(removal_reason)
|
||||
|
||||
"""
|
||||
return iter(self._removal_reason_list)
|
||||
|
||||
@_deprecate_args("message", "title")
|
||||
def add(self, *, message: str, title: str) -> RemovalReason:
|
||||
"""Add a removal reason to this subreddit.
|
||||
|
||||
:param message: The message associated with the removal reason.
|
||||
:param title: The title of the removal reason.
|
||||
|
||||
:returns: The :class:`.RemovalReason` added.
|
||||
|
||||
The message will be prepended with ``Hi u/username,`` automatically.
|
||||
|
||||
To add ``"Test"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.removal_reasons.add(title="Test", message="Foobar")
|
||||
|
||||
"""
|
||||
data = {"message": message, "title": title}
|
||||
url = API_PATH["removal_reasons_list"].format(subreddit=self.subreddit)
|
||||
reason_id = self._reddit.post(url, data=data)
|
||||
return RemovalReason(self._reddit, self.subreddit, reason_id)
|
||||
452
backend/venv/Lib/site-packages/praw/models/reddit/rules.py
Normal file
452
backend/venv/Lib/site-packages/praw/models/reddit/rules.py
Normal file
@@ -0,0 +1,452 @@
|
||||
"""Provide the Rule class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from urllib.parse import quote
|
||||
from warnings import warn
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import ClientException
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from .base import RedditBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Rule(RedditBase):
|
||||
"""An individual :class:`.Rule` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
==================== =============================================================
|
||||
Attribute Description
|
||||
==================== =============================================================
|
||||
``created_utc`` Time the rule was created, represented in `Unix Time`_.
|
||||
``description`` The description of the rule, if provided, otherwise a blank
|
||||
string.
|
||||
``kind`` The kind of rule. Can be ``"link"``, ``comment"``, or
|
||||
``"all"``.
|
||||
``priority`` Represents where the rule is ranked. For example, the first
|
||||
rule is at priority ``0``. Serves as an index number on the
|
||||
list of rules.
|
||||
``short_name`` The name of the rule.
|
||||
``violation_reason`` The reason that is displayed on the report menu for the rule.
|
||||
==================== =============================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "short_name"
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.rules.RuleModeration:
|
||||
"""Contain methods used to moderate rules.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
return RuleModeration(self)
|
||||
|
||||
def __getattribute__(self, attribute: str) -> Any:
|
||||
"""Get the value of an attribute."""
|
||||
value = super().__getattribute__(attribute)
|
||||
if attribute == "subreddit" and value is None:
|
||||
msg = "The Rule is missing a subreddit. File a bug report at PRAW."
|
||||
raise ValueError(msg)
|
||||
return value
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit | None = None,
|
||||
short_name: str | None = None,
|
||||
_data: dict[str, str] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Rule` instance."""
|
||||
if (short_name, _data).count(None) != 1:
|
||||
msg = "Either short_name or _data needs to be given."
|
||||
raise ValueError(msg)
|
||||
if short_name:
|
||||
self.short_name = short_name
|
||||
# Note: The subreddit parameter can be None, because the objector does not know
|
||||
# this info. In that case, it is the responsibility of the caller to set the
|
||||
# `subreddit` property on the returned value.
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def _fetch(self):
|
||||
for rule in self.subreddit.rules:
|
||||
if rule.short_name == self.short_name:
|
||||
self.__dict__.update(rule.__dict__)
|
||||
super()._fetch()
|
||||
return
|
||||
msg = f"Subreddit {self.subreddit} does not have the rule {self.short_name}"
|
||||
raise ClientException(msg)
|
||||
|
||||
|
||||
class RuleModeration:
|
||||
"""Contain methods used to moderate rules.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, rule: praw.models.Rule):
|
||||
"""Initialize a :class:`.RuleModeration` instance."""
|
||||
self.rule = rule
|
||||
|
||||
def delete(self):
|
||||
"""Delete a rule from this subreddit.
|
||||
|
||||
To delete ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules["No spam"].mod.delete()
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.rule.subreddit),
|
||||
"short_name": self.rule.short_name,
|
||||
}
|
||||
self.rule._reddit.post(API_PATH["remove_subreddit_rule"], data=data)
|
||||
|
||||
@_deprecate_args("description", "kind", "short_name", "violation_reason")
|
||||
def update(
|
||||
self,
|
||||
*,
|
||||
description: str | None = None,
|
||||
kind: str | None = None,
|
||||
short_name: str | None = None,
|
||||
violation_reason: str | None = None,
|
||||
) -> praw.models.Rule:
|
||||
"""Update the rule from this subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Existing values will be used for any unspecified arguments.
|
||||
|
||||
:param description: The new description for the rule. Can be empty.
|
||||
:param kind: The kind of item that the rule applies to. One of ``"link"``,
|
||||
``"comment"``, or ``"all"``.
|
||||
:param short_name: The name of the rule.
|
||||
:param violation_reason: The reason that is shown on the report menu.
|
||||
|
||||
:returns: A Rule object containing the updated values.
|
||||
|
||||
To update ``"No spam"`` from r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").removal_reasons["No spam"].mod.update(
|
||||
description="Don't do this!", violation_reason="Spam post"
|
||||
)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.rule.subreddit),
|
||||
"old_short_name": self.rule.short_name,
|
||||
}
|
||||
for name, value in {
|
||||
"description": description,
|
||||
"kind": kind,
|
||||
"short_name": short_name,
|
||||
"violation_reason": violation_reason,
|
||||
}.items():
|
||||
data[name] = getattr(self.rule, name) if value is None else value
|
||||
updated_rule = self.rule._reddit.post(
|
||||
API_PATH["update_subreddit_rule"], data=data
|
||||
)[0]
|
||||
updated_rule.subreddit = self.rule.subreddit
|
||||
return updated_rule
|
||||
|
||||
|
||||
class SubredditRules:
|
||||
"""Provide a set of functions to access a :class:`.Subreddit`'s rules.
|
||||
|
||||
For example, to list all the rules for a subreddit:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
Moderators can also add rules to the subreddit. For example, to make a rule called
|
||||
``"No spam"`` in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
"""
|
||||
|
||||
@cachedproperty
|
||||
def _rule_list(self) -> list[Rule]:
|
||||
"""Get a list of :class:`.Rule` objects.
|
||||
|
||||
:returns: A list of instances of :class:`.Rule`.
|
||||
|
||||
"""
|
||||
rule_list = self._reddit.get(API_PATH["rules"].format(subreddit=self.subreddit))
|
||||
for rule in rule_list:
|
||||
rule.subreddit = self.subreddit
|
||||
return rule_list
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> SubredditRulesModeration:
|
||||
"""Contain methods to moderate subreddit rules as a whole.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
To move the fourth rule to the first position, and then to move the prior first
|
||||
rule to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
return SubredditRulesModeration(self)
|
||||
|
||||
def __call__(self) -> list[praw.models.Rule]:
|
||||
r"""Return a list of :class:`.Rule`\ s (Deprecated).
|
||||
|
||||
:returns: A list of instances of :class:`.Rule`.
|
||||
|
||||
.. deprecated:: 7.1
|
||||
|
||||
Use the iterator by removing the call to :class:`.SubredditRules`. For
|
||||
example, in order to use the iterator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
"""
|
||||
warn(
|
||||
"Calling SubredditRules to get a list of rules is deprecated. Remove the"
|
||||
" parentheses to use the iterator. View the PRAW documentation on how to"
|
||||
" change the code in order to use the iterator"
|
||||
" (https://praw.readthedocs.io/en/latest/code_overview/other/subredditrules.html#praw.models.reddit.rules.SubredditRules.__call__).",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self._reddit.request(
|
||||
method="GET", path=API_PATH["rules"].format(subreddit=self.subreddit)
|
||||
)
|
||||
|
||||
def __getitem__(self, short_name: str | int | slice) -> praw.models.Rule:
|
||||
"""Return the :class:`.Rule` for the subreddit with short_name ``short_name``.
|
||||
|
||||
:param short_name: The short_name of the rule, or the rule number.
|
||||
|
||||
.. note::
|
||||
|
||||
Rules fetched using a specific rule name are lazily loaded, so you might
|
||||
have to access an attribute to get all the expected attributes.
|
||||
|
||||
This method is to be used to fetch a specific rule, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rule_name = "No spam"
|
||||
rule = reddit.subreddit("test").rules[rule_name]
|
||||
print(rule)
|
||||
|
||||
You can also fetch a numbered rule of a subreddit.
|
||||
|
||||
Rule numbers start at ``0``, so the first rule is at index ``0``, and the second
|
||||
rule is at index ``1``, and so on.
|
||||
|
||||
:raises: :py:class:`IndexError` if a rule of a specific number does not exist.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use negative indexes, such as ``-1``, to get the last rule. You can
|
||||
also use slices, to get a subset of rules, such as the last three rules with
|
||||
``rules[-3:]``.
|
||||
|
||||
For example, to fetch the second rule of r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
rule = reddit.subreddit("test").rules[1]
|
||||
|
||||
"""
|
||||
if not isinstance(short_name, str):
|
||||
return self._rule_list[short_name]
|
||||
return Rule(self._reddit, subreddit=self.subreddit, short_name=short_name)
|
||||
|
||||
def __init__(self, subreddit: praw.models.Subreddit):
|
||||
"""Initialize a :class:`.SubredditRules` instance.
|
||||
|
||||
:param subreddit: The subreddit whose rules to work with.
|
||||
|
||||
"""
|
||||
self.subreddit = subreddit
|
||||
self._reddit = subreddit._reddit
|
||||
|
||||
def __iter__(self) -> Iterator[praw.models.Rule]:
|
||||
"""Iterate through the rules of the subreddit.
|
||||
|
||||
:returns: An iterator containing all the rules of a subreddit.
|
||||
|
||||
This method is used to discover all rules for a subreddit.
|
||||
|
||||
For example, to get the rules for r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for rule in reddit.subreddit("test").rules:
|
||||
print(rule)
|
||||
|
||||
"""
|
||||
return iter(self._rule_list)
|
||||
|
||||
|
||||
class SubredditRulesModeration:
|
||||
"""Contain methods to moderate subreddit rules as a whole.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
To move the fourth rule to the first position, and then to move the prior first rule
|
||||
to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, subreddit_rules: SubredditRules):
|
||||
"""Initialize a :class:`.SubredditRulesModeration` instance."""
|
||||
self.subreddit_rules = subreddit_rules
|
||||
|
||||
@_deprecate_args("short_name", "kind", "description", "violation_reason")
|
||||
def add(
|
||||
self,
|
||||
*,
|
||||
description: str = "",
|
||||
kind: str,
|
||||
short_name: str,
|
||||
violation_reason: str | None = None,
|
||||
) -> praw.models.Rule:
|
||||
"""Add a removal reason to this subreddit.
|
||||
|
||||
:param description: The description for the rule.
|
||||
:param kind: The kind of item that the rule applies to. One of ``"link"``,
|
||||
``"comment"``, or ``"all"``.
|
||||
:param short_name: The name of the rule.
|
||||
:param violation_reason: The reason that is shown on the report menu. If a
|
||||
violation reason is not specified, the short name will be used as the
|
||||
violation reason.
|
||||
|
||||
:returns: The added :class:`.Rule`.
|
||||
|
||||
To add rule ``"No spam"`` to r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").rules.mod.add(
|
||||
short_name="No spam", kind="all", description="Do not spam. Spam bad"
|
||||
)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"r": str(self.subreddit_rules.subreddit),
|
||||
"description": description,
|
||||
"kind": kind,
|
||||
"short_name": short_name,
|
||||
"violation_reason": (
|
||||
short_name if violation_reason is None else violation_reason
|
||||
),
|
||||
}
|
||||
new_rule = self.subreddit_rules._reddit.post(
|
||||
API_PATH["add_subreddit_rule"], data=data
|
||||
)[0]
|
||||
new_rule.subreddit = self.subreddit_rules.subreddit
|
||||
return new_rule
|
||||
|
||||
def reorder(self, rule_list: list[praw.models.Rule]) -> list[praw.models.Rule]:
|
||||
"""Reorder the rules of a subreddit.
|
||||
|
||||
:param rule_list: The list of rules, in the wanted order. Each index of the list
|
||||
indicates the position of the rule.
|
||||
|
||||
:returns: A list containing the rules in the specified order.
|
||||
|
||||
For example, to move the fourth rule to the first position, and then to move the
|
||||
prior first rule to where the third rule originally was in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
rules = list(subreddit.rules)
|
||||
new_rules = rules[3:4] + rules[1:3] + rules[0:1] + rules[4:]
|
||||
# Alternate: [rules[3]] + rules[1:3] + [rules[0]] + rules[4:]
|
||||
new_rule_list = subreddit.rules.mod.reorder(new_rules)
|
||||
|
||||
"""
|
||||
order_string = quote(
|
||||
",".join([rule.short_name for rule in rule_list]), safe=","
|
||||
)
|
||||
data = {
|
||||
"r": str(self.subreddit_rules.subreddit),
|
||||
"new_rule_order": order_string,
|
||||
}
|
||||
response = self.subreddit_rules._reddit.post(
|
||||
API_PATH["reorder_subreddit_rules"], data=data
|
||||
)
|
||||
for rule in response:
|
||||
rule.subreddit = self.subreddit_rules.subreddit
|
||||
return response
|
||||
951
backend/venv/Lib/site-packages/praw/models/reddit/submission.py
Normal file
951
backend/venv/Lib/site-packages/praw/models/reddit/submission.py
Normal file
@@ -0,0 +1,951 @@
|
||||
"""Provide the Submission class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from json import dumps
|
||||
from typing import TYPE_CHECKING, Any, Generator
|
||||
from urllib.parse import urljoin
|
||||
from warnings import warn
|
||||
|
||||
from prawcore import Conflict
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...exceptions import InvalidURL
|
||||
from ...util import _deprecate_args, cachedproperty
|
||||
from ..comment_forest import CommentForest
|
||||
from ..listing.listing import Listing
|
||||
from ..listing.mixins import SubmissionListingMixin
|
||||
from .base import RedditBase
|
||||
from .mixins import FullnameMixin, ModNoteMixin, ThingModerationMixin, UserContentMixin
|
||||
from .poll import PollData
|
||||
from .redditor import Redditor
|
||||
from .subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
INLINE_MEDIA_PATTERN = re.compile(
|
||||
r"\n\n!?(\[.*?])?\(?((https://((preview|i)\.redd\.it|reddit.com/link).*?)|(?!https)([a-zA-Z0-9]+( \".*?\")?))\)?"
|
||||
)
|
||||
MEDIA_TYPE_MAPPING = {
|
||||
"Image": "img",
|
||||
"RedditVideo": "video",
|
||||
"AnimatedImage": "gif",
|
||||
}
|
||||
|
||||
|
||||
class SubmissionFlair:
|
||||
"""Provide a set of functions pertaining to :class:`.Submission` flair."""
|
||||
|
||||
def __init__(self, submission: praw.models.Submission):
|
||||
"""Initialize a :class:`.SubmissionFlair` instance.
|
||||
|
||||
:param submission: The :class:`.Submission` associated with the flair functions.
|
||||
|
||||
"""
|
||||
self.submission = submission
|
||||
|
||||
def choices(self) -> list[dict[str, bool | list | str]]:
|
||||
"""Return list of available flair choices.
|
||||
|
||||
Choices are required in order to use :meth:`.select`.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
|
||||
"""
|
||||
url = API_PATH["flairselector"].format(subreddit=self.submission.subreddit)
|
||||
return self.submission._reddit.post(
|
||||
url, data={"link": self.submission.fullname}
|
||||
)["choices"]
|
||||
|
||||
@_deprecate_args("flair_template_id", "text")
|
||||
def select(self, flair_template_id: str, *, text: str | None = None):
|
||||
"""Select flair for submission.
|
||||
|
||||
:param flair_template_id: The flair template to select. The possible values can
|
||||
be discovered through :meth:`.choices`.
|
||||
:param text: If the template's ``flair_text_editable`` value is ``True``, this
|
||||
value will set a custom text (default: ``None``).
|
||||
|
||||
For example, to select an arbitrary editable flair text (assuming there is one)
|
||||
and set a custom value try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
template_id = next(x for x in choices if x["flair_text_editable"])["flair_template_id"]
|
||||
submission.flair.select(template_id, text="my custom value")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"flair_template_id": flair_template_id,
|
||||
"link": self.submission.fullname,
|
||||
"text": text,
|
||||
}
|
||||
url = API_PATH["select_flair"].format(subreddit=self.submission.subreddit)
|
||||
self.submission._reddit.post(url, data=data)
|
||||
|
||||
|
||||
class SubmissionModeration(ThingModerationMixin, ModNoteMixin):
|
||||
"""Provide a set of functions pertaining to :class:`.Submission` moderation.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
|
||||
REMOVAL_MESSAGE_API = "removal_link_message"
|
||||
|
||||
def __init__(self, submission: praw.models.Submission):
|
||||
"""Initialize a :class:`.SubmissionModeration` instance.
|
||||
|
||||
:param submission: The submission to moderate.
|
||||
|
||||
"""
|
||||
self.thing = submission
|
||||
|
||||
@_deprecate_args("state")
|
||||
def contest_mode(self, *, state: bool = True):
|
||||
"""Set contest mode for the comments of this submission.
|
||||
|
||||
:param state: ``True`` enables contest mode and ``False`` disables (default:
|
||||
``True``).
|
||||
|
||||
Contest mode have the following effects:
|
||||
|
||||
- The comment thread will default to being sorted randomly.
|
||||
- Replies to top-level comments will be hidden behind "[show replies]" buttons.
|
||||
- Scores will be hidden from non-moderators.
|
||||
- Scores accessed through the API (mobile apps, bots) will be obscured to "1"
|
||||
for non-moderators.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.contest_mode()
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["contest_mode"], data={"id": self.thing.fullname, "state": state}
|
||||
)
|
||||
|
||||
@_deprecate_args("text", "css_class", "flair_template_id")
|
||||
def flair(
|
||||
self,
|
||||
*,
|
||||
css_class: str = "",
|
||||
flair_template_id: str | None = None,
|
||||
text: str = "",
|
||||
):
|
||||
"""Set flair for the submission.
|
||||
|
||||
:param css_class: The css class to associate with the flair html (default:
|
||||
``""``).
|
||||
:param flair_template_id: The flair template ID to use when flairing.
|
||||
:param text: The flair text to associate with the :class:`.Submission` (default:
|
||||
``""``).
|
||||
|
||||
This method can only be used by an authenticated user who is a moderator of the
|
||||
submission's :class:`.Subreddit`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.flair(text="PRAW", css_class="bot")
|
||||
|
||||
"""
|
||||
data = {
|
||||
"css_class": css_class,
|
||||
"link": self.thing.fullname,
|
||||
"text": text,
|
||||
}
|
||||
url = API_PATH["flair"].format(subreddit=self.thing.subreddit)
|
||||
if flair_template_id is not None:
|
||||
data["flair_template_id"] = flair_template_id
|
||||
url = API_PATH["select_flair"].format(subreddit=self.thing.subreddit)
|
||||
self.thing._reddit.post(url, data=data)
|
||||
|
||||
def nsfw(self):
|
||||
"""Mark as not safe for work.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("nsfw test", selftext="nsfw")
|
||||
submission.mod.nsfw()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.sfw`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["marknsfw"], data={"id": self.thing.fullname})
|
||||
|
||||
def set_original_content(self):
|
||||
"""Mark as original content.
|
||||
|
||||
This method can be used by moderators of the subreddit that the submission
|
||||
belongs to. If the subreddit has enabled the Original Content beta feature in
|
||||
settings, then the submission's author can use it as well.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("oc test", selftext="original")
|
||||
submission.mod.set_original_content()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unset_original_content`
|
||||
|
||||
"""
|
||||
data = {
|
||||
"id": self.thing.id,
|
||||
"fullname": self.thing.fullname,
|
||||
"should_set_oc": True,
|
||||
"executed": False,
|
||||
"r": self.thing.subreddit,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["set_original_content"], data=data)
|
||||
|
||||
def sfw(self):
|
||||
"""Mark as safe for work.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.sfw()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.nsfw`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["unmarknsfw"], data={"id": self.thing.fullname}
|
||||
)
|
||||
|
||||
def spoiler(self):
|
||||
"""Indicate that the submission contains spoilers.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.spoiler()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unspoiler`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["spoiler"], data={"id": self.thing.fullname})
|
||||
|
||||
@_deprecate_args("state", "bottom")
|
||||
def sticky(
|
||||
self, *, bottom: bool = True, state: bool = True
|
||||
) -> praw.models.Submission:
|
||||
"""Set the submission's sticky state in its subreddit.
|
||||
|
||||
:param bottom: When ``True``, set the submission as the bottom sticky. If no top
|
||||
sticky exists, this submission will become the top sticky regardless
|
||||
(default: ``True``).
|
||||
:param state: ``True`` sets the sticky for the submission and ``False`` unsets
|
||||
(default: ``True``).
|
||||
|
||||
:returns: The stickied submission object.
|
||||
|
||||
.. note::
|
||||
|
||||
When a submission is stickied two or more times, the Reddit API responds
|
||||
with a 409 error that is raised as a ``Conflict`` by prawcore. This method
|
||||
suppresses these ``Conflict`` errors.
|
||||
|
||||
This submission will replace the second stickied submission if one exists.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mod.sticky()
|
||||
|
||||
"""
|
||||
data = {"id": self.thing.fullname, "state": state}
|
||||
if not bottom:
|
||||
data["num"] = 1
|
||||
try:
|
||||
return self.thing._reddit.post(API_PATH["sticky_submission"], data=data)
|
||||
except Conflict:
|
||||
pass
|
||||
|
||||
@_deprecate_args("sort")
|
||||
def suggested_sort(self, *, sort: str = "blank"):
|
||||
"""Set the suggested sort for the comments of the submission.
|
||||
|
||||
:param sort: Can be one of: ``"confidence"``, ``"top"``, ``"new"``,
|
||||
``"controversial"``, ``"old"``, ``"random"``, ``"qa"``, or ``"blank"``
|
||||
(default: ``"blank"``).
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["suggested_sort"], data={"id": self.thing.fullname, "sort": sort}
|
||||
)
|
||||
|
||||
def unset_original_content(self):
|
||||
"""Indicate that the submission is not original content.
|
||||
|
||||
This method can be used by moderators of the subreddit that the submission
|
||||
belongs to. If the subreddit has enabled the Original Content beta feature in
|
||||
settings, then the submission's author can use it as well.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("oc test", selftext="original")
|
||||
submission.mod.unset_original_content()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.set_original_content`
|
||||
|
||||
"""
|
||||
data = {
|
||||
"id": self.thing.id,
|
||||
"fullname": self.thing.fullname,
|
||||
"should_set_oc": False,
|
||||
"executed": False,
|
||||
"r": self.thing.subreddit,
|
||||
}
|
||||
self.thing._reddit.post(API_PATH["set_original_content"], data=data)
|
||||
|
||||
def unspoiler(self):
|
||||
"""Indicate that the submission does not contain spoilers.
|
||||
|
||||
This method can be used both by the submission author and moderators of the
|
||||
subreddit that the submission belongs to.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.subreddit("test").submit("not spoiler", selftext="spoiler")
|
||||
submission.mod.unspoiler()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.spoiler`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(API_PATH["unspoiler"], data={"id": self.thing.fullname})
|
||||
|
||||
def update_crowd_control_level(self, level: int):
|
||||
"""Change the Crowd Control level of the submission.
|
||||
|
||||
:param level: An integer between 0 and 3.
|
||||
|
||||
**Level Descriptions**
|
||||
|
||||
===== ======== ================================================================
|
||||
Level Name Description
|
||||
===== ======== ================================================================
|
||||
0 Off Crowd Control will not action any of the submission's comments.
|
||||
1 Lenient Comments from users who have negative karma in the subreddit are
|
||||
automatically collapsed.
|
||||
2 Moderate Comments from new users and users with negative karma in the
|
||||
subreddit are automatically collapsed.
|
||||
3 Strict Comments from users who haven’t joined the subreddit, new users,
|
||||
and users with negative karma in the subreddit are automatically
|
||||
collapsed.
|
||||
===== ======== ================================================================
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("745ryj")
|
||||
submission.mod.update_crowd_control_level(2)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`~.CommentModeration.show`
|
||||
|
||||
"""
|
||||
self.thing._reddit.post(
|
||||
API_PATH["update_crowd_control"],
|
||||
data={"id": self.thing.fullname, "level": level},
|
||||
)
|
||||
|
||||
|
||||
class Submission(SubmissionListingMixin, UserContentMixin, FullnameMixin, RedditBase):
|
||||
"""A class for submissions to Reddit.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================== =========================================================
|
||||
Attribute Description
|
||||
========================== =========================================================
|
||||
``author`` Provides an instance of :class:`.Redditor`.
|
||||
``author_flair_text`` The text content of the author's flair, or ``None`` if
|
||||
not flaired.
|
||||
``clicked`` Whether or not the submission has been clicked by the
|
||||
client.
|
||||
``comments`` Provides an instance of :class:`.CommentForest`.
|
||||
``created_utc`` Time the submission was created, represented in `Unix
|
||||
Time`_.
|
||||
``distinguished`` Whether or not the submission is distinguished.
|
||||
``edited`` Whether or not the submission has been edited.
|
||||
``id`` ID of the submission.
|
||||
``is_original_content`` Whether or not the submission has been set as original
|
||||
content.
|
||||
``is_self`` Whether or not the submission is a selfpost (text-only).
|
||||
``link_flair_template_id`` The link flair's ID.
|
||||
``link_flair_text`` The link flair's text content, or ``None`` if not
|
||||
flaired.
|
||||
``locked`` Whether or not the submission has been locked.
|
||||
``name`` Fullname of the submission.
|
||||
``num_comments`` The number of comments on the submission.
|
||||
``over_18`` Whether or not the submission has been marked as NSFW.
|
||||
``permalink`` A permalink for the submission.
|
||||
``poll_data`` A :class:`.PollData` object representing the data of this
|
||||
submission, if it is a poll submission.
|
||||
``saved`` Whether or not the submission is saved.
|
||||
``score`` The number of upvotes for the submission.
|
||||
``selftext`` The submissions' selftext - an empty string if a link
|
||||
post.
|
||||
``spoiler`` Whether or not the submission has been marked as a
|
||||
spoiler.
|
||||
``stickied`` Whether or not the submission is stickied.
|
||||
``subreddit`` Provides an instance of :class:`.Subreddit`.
|
||||
``title`` The title of the submission.
|
||||
``upvote_ratio`` The percentage of upvotes from all votes on the
|
||||
submission.
|
||||
``url`` The URL the submission links to, or the permalink if a
|
||||
selfpost.
|
||||
========================== =========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
STR_FIELD = "id"
|
||||
|
||||
@staticmethod
|
||||
def id_from_url(url: str) -> str:
|
||||
"""Return the ID contained within a submission URL.
|
||||
|
||||
:param url: A url to a submission in one of the following formats (http urls
|
||||
will also work):
|
||||
|
||||
- ``"https://redd.it/2gmzqe"``
|
||||
- ``"https://reddit.com/comments/2gmzqe/"``
|
||||
- ``"https://www.reddit.com/r/redditdev/comments/2gmzqe/praw_https/"``
|
||||
- ``"https://www.reddit.com/gallery/2gmzqe"``
|
||||
|
||||
:raises: :class:`.InvalidURL` if ``url`` is not a valid submission URL.
|
||||
|
||||
"""
|
||||
parts = RedditBase._url_parts(url)
|
||||
if "comments" not in parts and "gallery" not in parts:
|
||||
submission_id = parts[-1]
|
||||
if "r" in parts:
|
||||
raise InvalidURL(
|
||||
url, message="Invalid URL (subreddit, not submission): {}"
|
||||
)
|
||||
|
||||
elif "gallery" in parts:
|
||||
submission_id = parts[parts.index("gallery") + 1]
|
||||
|
||||
elif parts[-1] == "comments":
|
||||
raise InvalidURL(url, message="Invalid URL (submission ID not present): {}")
|
||||
|
||||
else:
|
||||
submission_id = parts[parts.index("comments") + 1]
|
||||
|
||||
if not submission_id.isalnum():
|
||||
raise InvalidURL(url)
|
||||
return submission_id
|
||||
|
||||
@cachedproperty
|
||||
def flair(self) -> SubmissionFlair:
|
||||
"""Provide an instance of :class:`.SubmissionFlair`.
|
||||
|
||||
This attribute is used to work with flair as a regular user of the subreddit the
|
||||
submission belongs to. Moderators can directly use :meth:`.flair`.
|
||||
|
||||
For example, to select an arbitrary editable flair text (assuming there is one)
|
||||
and set a custom value try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
choices = submission.flair.choices()
|
||||
template_id = next(x for x in choices if x["flair_text_editable"])["flair_template_id"]
|
||||
submission.flair.select(template_id, text="my custom value")
|
||||
|
||||
"""
|
||||
return SubmissionFlair(self)
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> SubmissionModeration:
|
||||
"""Provide an instance of :class:`.SubmissionModeration`.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("8dmv8z")
|
||||
submission.mod.approve()
|
||||
|
||||
"""
|
||||
return SubmissionModeration(self)
|
||||
|
||||
@property
|
||||
def _kind(self) -> str:
|
||||
"""Return the class's kind."""
|
||||
return self._reddit.config.kinds["submission"]
|
||||
|
||||
@property
|
||||
def comments(self) -> CommentForest:
|
||||
"""Provide an instance of :class:`.CommentForest`.
|
||||
|
||||
This attribute can be used, for example, to obtain a flat list of comments, with
|
||||
any :class:`.MoreComments` removed:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission.comments.replace_more(limit=0)
|
||||
comments = submission.comments.list()
|
||||
|
||||
Sort order and comment limit can be set with the ``comment_sort`` and
|
||||
``comment_limit`` attributes before comments are fetched, including any call to
|
||||
:meth:`.replace_more`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission.comment_sort = "new"
|
||||
comments = submission.comments.list()
|
||||
|
||||
.. note::
|
||||
|
||||
The appropriate values for ``"comment_sort"`` include ``"confidence"``,
|
||||
``"controversial"``, ``"new"``, ``"old"``, ``"q&a"``, and ``"top"``
|
||||
|
||||
See :ref:`extracting_comments` for more on working with a
|
||||
:class:`.CommentForest`.
|
||||
|
||||
"""
|
||||
# This assumes _comments is set so that _fetch is called when it's not.
|
||||
return self._comments
|
||||
|
||||
@property
|
||||
def shortlink(self) -> str:
|
||||
"""Return a shortlink to the submission.
|
||||
|
||||
For example, https://redd.it/eorhm is a shortlink for
|
||||
https://www.reddit.com/r/announcements/comments/eorhm/reddit_30_less_typing/.
|
||||
|
||||
"""
|
||||
return urljoin(self._reddit.config.short_url, self.id)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
id: str | None = None,
|
||||
url: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.Submission` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param id: A reddit base36 submission ID, e.g., ``"2gmzqe"``.
|
||||
:param url: A URL supported by :meth:`.id_from_url`.
|
||||
|
||||
Either ``id`` or ``url`` can be provided, but not both.
|
||||
|
||||
"""
|
||||
if (id, url, _data).count(None) != 2:
|
||||
msg = "Exactly one of 'id', 'url', or '_data' must be provided."
|
||||
raise TypeError(msg)
|
||||
self.comment_limit = 2048
|
||||
|
||||
# Specify the sort order for ``comments``
|
||||
self.comment_sort = "confidence"
|
||||
|
||||
if id:
|
||||
self.id = id
|
||||
elif url:
|
||||
self.id = self.id_from_url(url)
|
||||
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
self._additional_fetch_params = {}
|
||||
self._comments_by_id = {}
|
||||
|
||||
def __setattr__(self, attribute: str, value: Any):
|
||||
"""Objectify author, subreddit, and poll data attributes."""
|
||||
if attribute == "author":
|
||||
value = Redditor.from_data(self._reddit, value)
|
||||
elif attribute == "subreddit":
|
||||
value = Subreddit(self._reddit, value)
|
||||
elif attribute == "poll_data":
|
||||
value = PollData(self._reddit, value)
|
||||
elif (
|
||||
attribute == "comment_sort"
|
||||
and hasattr(self, "_fetched")
|
||||
and self._fetched
|
||||
and hasattr(self, "_reddit")
|
||||
and self._reddit.config.warn_comment_sort
|
||||
):
|
||||
warn(
|
||||
"The comments for this submission have already been fetched, so the"
|
||||
" updated comment_sort will not have any effect.",
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__setattr__(attribute, value)
|
||||
|
||||
def _chunk(
|
||||
self,
|
||||
*,
|
||||
chunk_size: int,
|
||||
other_submissions: list[praw.models.Submission] | None,
|
||||
) -> Generator[str, None, None]:
|
||||
all_submissions = [self.fullname]
|
||||
if other_submissions:
|
||||
all_submissions += [x.fullname for x in other_submissions]
|
||||
|
||||
for position in range(0, len(all_submissions), chunk_size):
|
||||
yield ",".join(all_submissions[position : position + 50])
|
||||
|
||||
def _edit_experimental(
|
||||
self,
|
||||
body: str,
|
||||
*,
|
||||
preserve_inline_media: bool = False,
|
||||
inline_media: dict[str, praw.models.InlineMedia] | None = None,
|
||||
) -> praw.models.Submission:
|
||||
"""Replace the body of the object with ``body``.
|
||||
|
||||
:param body: The Markdown formatted content for the updated object.
|
||||
:param preserve_inline_media: Attempt to preserve inline media in ``body``.
|
||||
|
||||
.. danger::
|
||||
|
||||
This method is experimental. It is reliant on undocumented API endpoints
|
||||
and may result in existing inline media not displaying correctly and/or
|
||||
creating a malformed body. Use at your own risk. This method may be
|
||||
removed in the future without warning.
|
||||
|
||||
:param inline_media: A dict of :class:`.InlineMedia` objects where the key is
|
||||
the placeholder name in ``body``.
|
||||
|
||||
:returns: The current instance after updating its attributes.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from praw.models import InlineGif, InlineImage, InlineVideo
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
gif = InlineGif(path="path/to/image.gif", caption="optional caption")
|
||||
image = InlineImage(path="path/to/image.jpg", caption="optional caption")
|
||||
video = InlineVideo(path="path/to/video.mp4", caption="optional caption")
|
||||
body = "New body with a gif {gif1} an image {image1} and a video {video1} inline"
|
||||
media = {"gif1": gif, "image1": image, "video1": video}
|
||||
submission._edit_experimental(submission.selftext + body, inline_media=media)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"thing_id": self.fullname,
|
||||
"validate_on_submit": self._reddit.validate_on_submit,
|
||||
}
|
||||
is_richtext_json = False
|
||||
if INLINE_MEDIA_PATTERN.search(body) and self.media_metadata:
|
||||
is_richtext_json = True
|
||||
if inline_media:
|
||||
body = body.format(
|
||||
**{
|
||||
placeholder: self.subreddit._upload_inline_media(media)
|
||||
for placeholder, media in inline_media.items()
|
||||
}
|
||||
)
|
||||
is_richtext_json = True
|
||||
if is_richtext_json:
|
||||
richtext_json = self.subreddit._convert_to_fancypants(body)
|
||||
if preserve_inline_media:
|
||||
self._replace_richtext_links(richtext_json)
|
||||
data["richtext_json"] = dumps(richtext_json)
|
||||
else:
|
||||
data["text"] = body
|
||||
updated = self._reddit.post(API_PATH["edit"], data=data)
|
||||
if not is_richtext_json:
|
||||
updated = updated[0]
|
||||
for attribute in [
|
||||
"_fetched",
|
||||
"_reddit",
|
||||
"_submission",
|
||||
"replies",
|
||||
"subreddit",
|
||||
]:
|
||||
if attribute in updated.__dict__:
|
||||
delattr(updated, attribute)
|
||||
self.__dict__.update(updated.__dict__)
|
||||
else:
|
||||
self.__dict__.update(updated)
|
||||
return self
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
submission_listing, comment_listing = data
|
||||
comment_listing = Listing(self._reddit, _data=comment_listing["data"])
|
||||
|
||||
submission_data = submission_listing["data"]["children"][0]["data"]
|
||||
submission = type(self)(self._reddit, _data=submission_data)
|
||||
delattr(submission, "comment_limit")
|
||||
delattr(submission, "comment_sort")
|
||||
submission._comments = CommentForest(self)
|
||||
|
||||
self.__dict__.update(submission.__dict__)
|
||||
self.comments._update(comment_listing.children)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_data(self):
|
||||
name, fields, params = self._fetch_info()
|
||||
params.update(self._additional_fetch_params.copy())
|
||||
path = API_PATH[name].format(**fields)
|
||||
return self._reddit.request(method="GET", params=params, path=path)
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"submission",
|
||||
{"id": self.id},
|
||||
{"limit": self.comment_limit, "sort": self.comment_sort},
|
||||
)
|
||||
|
||||
def _replace_richtext_links(self, richtext_json: dict):
|
||||
parsed_media_types = {
|
||||
media_id: MEDIA_TYPE_MAPPING[value["e"]]
|
||||
for media_id, value in self.media_metadata.items()
|
||||
}
|
||||
|
||||
for index, element in enumerate(richtext_json["document"][:]):
|
||||
element_items = element.get("c")
|
||||
if isinstance(element_items, str):
|
||||
assert element.get("e") in ["gif", "img", "video"], (
|
||||
"Unexpected richtext JSON schema. Please file a bug report with"
|
||||
" PRAW."
|
||||
) # make sure this is an inline element
|
||||
continue # pragma: no cover
|
||||
for item in element.get("c"):
|
||||
if item.get("e") == "link":
|
||||
ids = set(parsed_media_types)
|
||||
# remove extra bits from the url
|
||||
url = item["u"].split("https://")[1].split("?")[0]
|
||||
# the id is in the url somewhere, so we split by '/' and '.'
|
||||
matched_id = ids.intersection(re.split(r"[./]", url))
|
||||
if matched_id:
|
||||
matched_id = matched_id.pop()
|
||||
correct_element = {
|
||||
"e": parsed_media_types[matched_id],
|
||||
"id": matched_id,
|
||||
}
|
||||
if item.get("t") != item.get("u"): # add caption if it exists
|
||||
correct_element["c"] = item["t"]
|
||||
richtext_json["document"][index] = correct_element
|
||||
|
||||
def add_fetch_param(self, key: str, value: str):
|
||||
"""Add a parameter to be used for the next fetch.
|
||||
|
||||
:param key: The key of the fetch parameter.
|
||||
:param value: The value of the fetch parameter.
|
||||
|
||||
For example, to fetch a submission with the ``rtjson`` attribute populated:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("mcqjl8")
|
||||
submission.add_fetch_param("rtj", "all")
|
||||
print(submission.rtjson)
|
||||
|
||||
"""
|
||||
if (
|
||||
hasattr(self, "_fetched")
|
||||
and self._fetched
|
||||
and hasattr(self, "_reddit")
|
||||
and self._reddit.config.warn_additional_fetch_params
|
||||
):
|
||||
warn(
|
||||
f"This {self.__class__.__name__.lower()} has already been fetched, so"
|
||||
" adding additional fetch parameters will not have any effect.",
|
||||
stacklevel=2,
|
||||
)
|
||||
self._additional_fetch_params[key] = value
|
||||
|
||||
@_deprecate_args(
|
||||
"subreddit",
|
||||
"title",
|
||||
"send_replies",
|
||||
"flair_id",
|
||||
"flair_text",
|
||||
"nsfw",
|
||||
"spoiler",
|
||||
)
|
||||
def crosspost(
|
||||
self,
|
||||
subreddit: praw.models.Subreddit,
|
||||
*,
|
||||
flair_id: str | None = None,
|
||||
flair_text: str | None = None,
|
||||
nsfw: bool = False,
|
||||
send_replies: bool = True,
|
||||
spoiler: bool = False,
|
||||
title: str | None = None,
|
||||
) -> praw.models.Submission:
|
||||
"""Crosspost the submission to a subreddit.
|
||||
|
||||
.. note::
|
||||
|
||||
Be aware you have to be subscribed to the target subreddit.
|
||||
|
||||
:param subreddit: Name of the subreddit or :class:`.Subreddit` object to
|
||||
crosspost into.
|
||||
:param flair_id: The flair template to select (default: ``None``).
|
||||
:param flair_text: If the template's ``flair_text_editable`` value is ``True``,
|
||||
this value will set a custom text (default: ``None``).
|
||||
:param nsfw: Whether the submission should be marked NSFW (default: ``False``).
|
||||
:param send_replies: When ``True``, messages will be sent to the created
|
||||
submission's author when comments are made to the submission (default:
|
||||
``True``).
|
||||
:param spoiler: Whether the submission should be marked as a spoiler (default:
|
||||
``False``).
|
||||
:param title: Title of the submission. Will use this submission's title if
|
||||
``None`` (default: ``None``).
|
||||
|
||||
:returns: A :class:`.Submission` object for the newly created submission.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
cross_post = submission.crosspost("learnprogramming", send_replies=False)
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.hide`
|
||||
|
||||
"""
|
||||
if title is None:
|
||||
title = self.title
|
||||
|
||||
data = {
|
||||
"sr": str(subreddit),
|
||||
"title": title,
|
||||
"sendreplies": bool(send_replies),
|
||||
"kind": "crosspost",
|
||||
"crosspost_fullname": self.fullname,
|
||||
"nsfw": bool(nsfw),
|
||||
"spoiler": bool(spoiler),
|
||||
}
|
||||
for key, value in (("flair_id", flair_id), ("flair_text", flair_text)):
|
||||
if value is not None:
|
||||
data[key] = value
|
||||
|
||||
return self._reddit.post(API_PATH["submit"], data=data)
|
||||
|
||||
@_deprecate_args("other_submissions")
|
||||
def hide(self, *, other_submissions: list[praw.models.Submission] | None = None):
|
||||
"""Hide :class:`.Submission`.
|
||||
|
||||
:param other_submissions: When provided, additionally hide this list of
|
||||
:class:`.Submission` instances as part of a single request (default:
|
||||
``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.hide()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.unhide`
|
||||
|
||||
"""
|
||||
for submissions in self._chunk(
|
||||
chunk_size=50, other_submissions=other_submissions
|
||||
):
|
||||
self._reddit.post(API_PATH["hide"], data={"id": submissions})
|
||||
|
||||
def mark_visited(self):
|
||||
"""Mark submission as visited.
|
||||
|
||||
This method requires a subscription to reddit premium.
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.mark_visited()
|
||||
|
||||
"""
|
||||
data = {"links": self.fullname}
|
||||
self._reddit.post(API_PATH["store_visits"], data=data)
|
||||
|
||||
@_deprecate_args("other_submissions")
|
||||
def unhide(self, *, other_submissions: list[praw.models.Submission] | None = None):
|
||||
"""Unhide :class:`.Submission`.
|
||||
|
||||
:param other_submissions: When provided, additionally unhide this list of
|
||||
:class:`.Submission` instances as part of a single request (default:
|
||||
``None``).
|
||||
|
||||
Example usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = reddit.submission("5or86n")
|
||||
submission.unhide()
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.hide`
|
||||
|
||||
"""
|
||||
for submissions in self._chunk(
|
||||
chunk_size=50, other_submissions=other_submissions
|
||||
):
|
||||
self._reddit.post(API_PATH["unhide"], data={"id": submissions})
|
||||
|
||||
|
||||
Subreddit._submission_class = Submission
|
||||
4255
backend/venv/Lib/site-packages/praw/models/reddit/subreddit.py
Normal file
4255
backend/venv/Lib/site-packages/praw/models/reddit/subreddit.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
"""Provide the :class:`.UserSubreddit` class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
from warnings import warn
|
||||
|
||||
from ...util.cache import cachedproperty
|
||||
from .subreddit import Subreddit, SubredditModeration
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class UserSubreddit(Subreddit):
|
||||
"""A class for :class:`.User` Subreddits.
|
||||
|
||||
To obtain an instance of this class execute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.user.me().subreddit
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
========================= ==========================================================
|
||||
Attribute Description
|
||||
========================= ==========================================================
|
||||
``can_assign_link_flair`` Whether users can assign their own link flair.
|
||||
``can_assign_user_flair`` Whether users can assign their own user flair.
|
||||
``created_utc`` Time the subreddit was created, represented in `Unix
|
||||
Time`_.
|
||||
``description`` Subreddit description, in Markdown.
|
||||
``description_html`` Subreddit description, in HTML.
|
||||
``display_name`` Name of the subreddit.
|
||||
``id`` ID of the subreddit.
|
||||
``name`` Fullname of the subreddit.
|
||||
``over18`` Whether the subreddit is NSFW.
|
||||
``public_description`` Description of the subreddit, shown in searches and on the
|
||||
"You must be invited to visit this community" page (if
|
||||
applicable).
|
||||
``spoilers_enabled`` Whether the spoiler tag feature is enabled.
|
||||
``subscribers`` Count of subscribers. This will be ``0`` unless unless the
|
||||
authenticated user is a moderator.
|
||||
``user_is_banned`` Whether the authenticated user is banned.
|
||||
``user_is_moderator`` Whether the authenticated user is a moderator.
|
||||
``user_is_subscriber`` Whether the authenticated user is subscribed.
|
||||
========================= ==========================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _dict_deprecated_wrapper(func: Callable) -> Callable:
|
||||
"""Show deprecation notice for dict only methods."""
|
||||
|
||||
def wrapper(*args: Any, **kwargs: Any):
|
||||
warn(
|
||||
"'Redditor.subreddit' is no longer a dict and is now an UserSubreddit"
|
||||
f" object. Using '{func.__name__}' is deprecated and will be removed in"
|
||||
" PRAW 8.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> praw.models.reddit.user_subreddit.UserSubredditModeration:
|
||||
"""Provide an instance of :class:`.UserSubredditModeration`.
|
||||
|
||||
For example, to update the authenticated user's display name:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.user.me().subreddit.mod.update(title="New display name")
|
||||
|
||||
"""
|
||||
return UserSubredditModeration(self)
|
||||
|
||||
def __getitem__(self, item: str) -> Any:
|
||||
"""Show deprecation notice for dict method ``__getitem__``."""
|
||||
warn(
|
||||
"'Redditor.subreddit' is no longer a dict and is now an UserSubreddit"
|
||||
" object. Accessing attributes using string indices is deprecated.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return getattr(self, item)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, *args: Any, **kwargs: Any):
|
||||
"""Initialize an :class:`.UserSubreddit` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
.. note::
|
||||
|
||||
This class should not be initialized directly. Instead, obtain an instance
|
||||
via: ``reddit.user.me().subreddit`` or
|
||||
``reddit.redditor("redditor_name").subreddit``.
|
||||
|
||||
"""
|
||||
|
||||
def predicate(item: str):
|
||||
name = getattr(item, "__name__", None)
|
||||
return name not in dir(object) + dir(Subreddit) and name in dir(dict)
|
||||
|
||||
for name, _member in inspect.getmembers(dict, predicate=predicate):
|
||||
if name != "__getitem__":
|
||||
setattr(
|
||||
self,
|
||||
name,
|
||||
self._dict_deprecated_wrapper(getattr(self.__dict__, name)),
|
||||
)
|
||||
|
||||
super().__init__(reddit, *args, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyIncorrectDocstring
|
||||
class UserSubredditModeration(SubredditModeration):
|
||||
"""Provides a set of moderation functions to a :class:`.UserSubreddit`.
|
||||
|
||||
For example, to accept a moderation invite from the user subreddit of u/spez:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").mod.accept_invite()
|
||||
|
||||
"""
|
||||
|
||||
def update(self, **settings: str | int | bool) -> dict[str, str | int | bool]:
|
||||
"""Update the :class:`.Subreddit`'s settings.
|
||||
|
||||
:param all_original_content: Mandate all submissions to be original content
|
||||
only.
|
||||
:param allow_chat_post_creation: Allow users to create chat submissions.
|
||||
:param allow_images: Allow users to upload images using the native image
|
||||
hosting.
|
||||
:param allow_polls: Allow users to post polls to the subreddit.
|
||||
:param allow_post_crossposts: Allow users to crosspost submissions from other
|
||||
subreddits.
|
||||
:param allow_top: Allow the subreddit to appear on r/all as well as the default
|
||||
and trending lists.
|
||||
:param allow_videos: Allow users to upload videos using the native image
|
||||
hosting.
|
||||
:param collapse_deleted_comments: Collapse deleted and removed comments on
|
||||
comments pages by default.
|
||||
:param crowd_control_chat_level: Controls the crowd control level for chat
|
||||
rooms. Goes from 0-3.
|
||||
:param crowd_control_level: Controls the crowd control level for submissions.
|
||||
Goes from 0-3.
|
||||
:param crowd_control_mode: Enables/disables crowd control.
|
||||
:param comment_score_hide_mins: The number of minutes to hide comment scores.
|
||||
:param description: Shown in the sidebar of your subreddit.
|
||||
:param disable_contributor_requests: Specifies whether redditors may send
|
||||
automated modmail messages requesting approval as a submitter.
|
||||
:param exclude_banned_modqueue: Exclude posts by site-wide banned users from
|
||||
modqueue/unmoderated.
|
||||
:param free_form_reports: Allow users to specify custom reasons in the report
|
||||
menu.
|
||||
:param header_hover_text: The text seen when hovering over the snoo.
|
||||
:param hide_ads: Don't show ads within this subreddit. Only applies to
|
||||
Premium-user only subreddits.
|
||||
:param key_color: A 6-digit rgb hex color (e.g., ``"#AABBCC"``), used as a
|
||||
thematic color for your subreddit on mobile.
|
||||
:param lang: A valid IETF language tag (underscore separated).
|
||||
:param link_type: The types of submissions users can make. One of ``"any"``,
|
||||
``"link"``, or ``"self"``.
|
||||
:param original_content_tag_enabled: Enables the use of the ``original content``
|
||||
label for submissions.
|
||||
:param over_18: Viewers must be over 18 years old (i.e., NSFW).
|
||||
:param public_description: Public description blurb. Appears in search results
|
||||
and on the landing page for private subreddits.
|
||||
:param public_traffic: Make the traffic stats page public.
|
||||
:param restrict_commenting: Specifies whether approved users have the ability to
|
||||
comment.
|
||||
:param restrict_posting: Specifies whether approved users have the ability to
|
||||
submit posts.
|
||||
:param show_media: Show thumbnails on submissions.
|
||||
:param show_media_preview: Expand media previews on comments pages.
|
||||
:param spam_comments: Spam filter strength for comments. One of ``"all"``,
|
||||
``"low"``, or ``"high"``.
|
||||
:param spam_links: Spam filter strength for links. One of ``"all"``, ``"low"``,
|
||||
or ``"high"``.
|
||||
:param spam_selfposts: Spam filter strength for selfposts. One of ``"all"``,
|
||||
``"low"``, or ``"high"``.
|
||||
:param spoilers_enabled: Enable marking posts as containing spoilers.
|
||||
:param submit_link_label: Custom label for submit link button (None for
|
||||
default).
|
||||
:param submit_text: Text to show on submission page.
|
||||
:param submit_text_label: Custom label for submit text post button (None for
|
||||
default).
|
||||
:param subreddit_type: The string ``"user"``.
|
||||
:param suggested_comment_sort: All comment threads will use this sorting method
|
||||
by default. Leave ``None``, or choose one of ``confidence``,
|
||||
``"controversial"``, ``"live"``, ``"new"``, ``"old"``, ``"qa"``,
|
||||
``"random"``, or ``"top"``.
|
||||
:param title: The title of the subreddit.
|
||||
:param welcome_message_enabled: Enables the subreddit welcome message.
|
||||
:param welcome_message_text: The text to be used as a welcome message. A welcome
|
||||
message is sent to all new subscribers by a Reddit bot.
|
||||
:param wiki_edit_age: Account age, in days, required to edit and create wiki
|
||||
pages.
|
||||
:param wiki_edit_karma: Subreddit karma required to edit and create wiki pages.
|
||||
:param wikimode: One of ``"anyone"``, ``"disabled"``, or ``"modonly"``.
|
||||
|
||||
Additional keyword arguments can be provided to handle new settings as Reddit
|
||||
introduces them.
|
||||
|
||||
Settings that are documented here and aren't explicitly set by you in a call to
|
||||
:meth:`.SubredditModeration.update` should retain their current value. If they
|
||||
do not please file a bug.
|
||||
|
||||
.. warning::
|
||||
|
||||
Undocumented settings, or settings that were very recently documented, may
|
||||
not retain their current value when updating. This often occurs when Reddit
|
||||
adds a new setting but forgets to add that setting to the API endpoint that
|
||||
is used to fetch the current settings.
|
||||
|
||||
"""
|
||||
current_settings = self.settings()
|
||||
|
||||
# These attributes come out using different names than they go in.
|
||||
remap = {
|
||||
"allow_top": "default_set",
|
||||
"header_title": "header_hover_text",
|
||||
"lang": "language",
|
||||
"link_type": "content_options",
|
||||
"sr": "subreddit_id",
|
||||
"type": "subreddit_type",
|
||||
}
|
||||
for new, old in remap.items():
|
||||
current_settings[new] = current_settings.pop(old)
|
||||
|
||||
current_settings.update(settings)
|
||||
return UserSubreddit._create_or_update(
|
||||
_reddit=self.subreddit._reddit, **current_settings
|
||||
)
|
||||
1871
backend/venv/Lib/site-packages/praw/models/reddit/widgets.py
Normal file
1871
backend/venv/Lib/site-packages/praw/models/reddit/widgets.py
Normal file
File diff suppressed because it is too large
Load Diff
346
backend/venv/Lib/site-packages/praw/models/reddit/wikipage.py
Normal file
346
backend/venv/Lib/site-packages/praw/models/reddit/wikipage.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""Provide the WikiPage class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Generator, Iterator
|
||||
|
||||
from ...const import API_PATH
|
||||
from ...util import _deprecate_args
|
||||
from ...util.cache import cachedproperty
|
||||
from ..listing.generator import ListingGenerator
|
||||
from .base import RedditBase
|
||||
from .redditor import Redditor
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class WikiPageModeration:
|
||||
"""Provides a set of moderation functions for a :class:`.WikiPage`.
|
||||
|
||||
For example, to add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, wikipage: WikiPage):
|
||||
"""Initialize a :class:`.WikiPageModeration` instance.
|
||||
|
||||
:param wikipage: The wikipage to moderate.
|
||||
|
||||
"""
|
||||
self.wikipage = wikipage
|
||||
|
||||
def add(self, redditor: praw.models.Redditor):
|
||||
"""Add an editor to this :class:`.WikiPage`.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
|
||||
To add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
data = {"page": self.wikipage.name, "username": str(redditor)}
|
||||
url = API_PATH["wiki_page_editor"].format(
|
||||
subreddit=self.wikipage.subreddit, method="add"
|
||||
)
|
||||
self.wikipage._reddit.post(url, data=data)
|
||||
|
||||
def remove(self, redditor: praw.models.Redditor):
|
||||
"""Remove an editor from this :class:`.WikiPage`.
|
||||
|
||||
:param redditor: A redditor name or :class:`.Redditor` instance.
|
||||
|
||||
To remove u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.remove("spez")
|
||||
|
||||
"""
|
||||
data = {"page": self.wikipage.name, "username": str(redditor)}
|
||||
url = API_PATH["wiki_page_editor"].format(
|
||||
subreddit=self.wikipage.subreddit, method="del"
|
||||
)
|
||||
self.wikipage._reddit.post(url, data=data)
|
||||
|
||||
def revert(self):
|
||||
"""Revert a wikipage back to a specific revision.
|
||||
|
||||
To revert the page ``"praw_test"`` in r/test to revision ``"1234abc"``, try
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].revision("1234abc").mod.revert()
|
||||
|
||||
.. note::
|
||||
|
||||
When you attempt to revert the page ``config/stylesheet``, Reddit checks to
|
||||
see if the revision being reverted to passes the CSS filter. If the check
|
||||
fails, then the revision attempt will also fail, and a
|
||||
``prawcore.Forbidden`` exception will be raised. For example, you can't
|
||||
revert to a revision that contains a link to ``url(%%PRAW%%)`` if there is
|
||||
no image named ``PRAW`` on the current stylesheet.
|
||||
|
||||
Here is an example of how to look for this type of error:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from prawcore.exceptions import Forbidden
|
||||
|
||||
try:
|
||||
reddit.subreddit("test").wiki["config/stylesheet"].revision("1234abc").mod.revert()
|
||||
except Forbidden as exception:
|
||||
try:
|
||||
exception.response.json()
|
||||
except ValueError:
|
||||
exception.response.text
|
||||
|
||||
If the error occurs, the output will look something like
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
{"reason": "INVALID_CSS", "message": "Forbidden", "explanation": "%(css_error)s"}
|
||||
|
||||
"""
|
||||
self.wikipage._reddit.post(
|
||||
API_PATH["wiki_revert"].format(subreddit=self.wikipage.subreddit),
|
||||
data={
|
||||
"page": self.wikipage.name,
|
||||
"revision": self.wikipage._revision,
|
||||
},
|
||||
)
|
||||
|
||||
def settings(self) -> dict[str, Any]:
|
||||
"""Return the settings for this :class:`.WikiPage`."""
|
||||
url = API_PATH["wiki_page_settings"].format(
|
||||
subreddit=self.wikipage.subreddit, page=self.wikipage.name
|
||||
)
|
||||
return self.wikipage._reddit.get(url)["data"]
|
||||
|
||||
@_deprecate_args("listed", "permlevel")
|
||||
def update(
|
||||
self, *, listed: bool, permlevel: int, **other_settings: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Update the settings for this :class:`.WikiPage`.
|
||||
|
||||
:param listed: Show this page on page list.
|
||||
:param permlevel: Who can edit this page? ``0`` use subreddit wiki permissions,
|
||||
``1`` only approved wiki contributors for this page may edit (see
|
||||
:meth:`.WikiPageModeration.add`), ``2`` only mods may edit and view.
|
||||
:param other_settings: Additional keyword arguments to pass.
|
||||
|
||||
:returns: The updated WikiPage settings.
|
||||
|
||||
To set the wikipage ``"praw_test"`` in r/test to mod only and disable it from
|
||||
showing in the page list, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.update(listed=False, permlevel=2)
|
||||
|
||||
"""
|
||||
other_settings.update({"listed": listed, "permlevel": permlevel})
|
||||
url = API_PATH["wiki_page_settings"].format(
|
||||
subreddit=self.wikipage.subreddit, page=self.wikipage.name
|
||||
)
|
||||
return self.wikipage._reddit.post(url, data=other_settings)["data"]
|
||||
|
||||
|
||||
class WikiPage(RedditBase):
|
||||
"""An individual :class:`.WikiPage` object.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
================= =================================================================
|
||||
Attribute Description
|
||||
================= =================================================================
|
||||
``content_html`` The contents of the wiki page, as HTML.
|
||||
``content_md`` The contents of the wiki page, as Markdown.
|
||||
``may_revise`` A ``bool`` representing whether or not the authenticated user may
|
||||
edit the wiki page.
|
||||
``name`` The name of the wiki page.
|
||||
``revision_by`` The :class:`.Redditor` who authored this revision of the wiki
|
||||
page.
|
||||
``revision_date`` The time of this revision, in `Unix Time`_.
|
||||
``subreddit`` The :class:`.Subreddit` this wiki page belongs to.
|
||||
================= =================================================================
|
||||
|
||||
.. _unix time: https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
"""
|
||||
|
||||
__hash__ = RedditBase.__hash__
|
||||
|
||||
@staticmethod
|
||||
def _revision_generator(
|
||||
*,
|
||||
generator_kwargs: dict[str, Any],
|
||||
subreddit: praw.models.Subreddit,
|
||||
url: str,
|
||||
) -> Generator[
|
||||
dict[str, Redditor | WikiPage | str | int | bool | None], None, None
|
||||
]:
|
||||
for revision in ListingGenerator(subreddit._reddit, url, **generator_kwargs):
|
||||
if revision["author"] is not None:
|
||||
revision["author"] = Redditor(
|
||||
subreddit._reddit, _data=revision["author"]["data"]
|
||||
)
|
||||
revision["page"] = WikiPage(
|
||||
subreddit._reddit, subreddit, revision["page"], revision["id"]
|
||||
)
|
||||
yield revision
|
||||
|
||||
@cachedproperty
|
||||
def mod(self) -> WikiPageModeration:
|
||||
"""Provide an instance of :class:`.WikiPageModeration`.
|
||||
|
||||
For example, to add u/spez as an editor on the wikipage ``"praw_test"`` try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.subreddit("test").wiki["praw_test"].mod.add("spez")
|
||||
|
||||
"""
|
||||
return WikiPageModeration(self)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reddit: praw.Reddit,
|
||||
subreddit: praw.models.Subreddit,
|
||||
name: str,
|
||||
revision: str | None = None,
|
||||
_data: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Initialize a :class:`.WikiPage` instance.
|
||||
|
||||
:param revision: A specific revision ID to fetch. By default, fetches the most
|
||||
recent revision.
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self._revision = revision
|
||||
self.subreddit = subreddit
|
||||
super().__init__(reddit, _data=_data, _str_field=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return (
|
||||
f"{self.__class__.__name__}(subreddit={self.subreddit!r},"
|
||||
f" name={self.name!r})"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representation of the instance."""
|
||||
return f"{self.subreddit}/{self.name}"
|
||||
|
||||
def _fetch(self):
|
||||
data = self._fetch_data()
|
||||
data = data["data"]
|
||||
if data["revision_by"] is not None:
|
||||
data["revision_by"] = Redditor(
|
||||
self._reddit, _data=data["revision_by"]["data"]
|
||||
)
|
||||
self.__dict__.update(data)
|
||||
super()._fetch()
|
||||
|
||||
def _fetch_info(self):
|
||||
return (
|
||||
"wiki_page",
|
||||
{"subreddit": self.subreddit, "page": self.name},
|
||||
{"v": self._revision} if self._revision else None,
|
||||
)
|
||||
|
||||
def discussions(self, **generator_kwargs: Any) -> Iterator[praw.models.Submission]:
|
||||
"""Return a :class:`.ListingGenerator` for discussions of a wiki page.
|
||||
|
||||
Discussions are site-wide links to a wiki page.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To view the titles of discussions of the page ``"praw_test"`` in r/test, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for submission in reddit.subreddit("test").wiki["praw_test"].discussions():
|
||||
print(submission.title)
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit,
|
||||
API_PATH["wiki_discussions"].format(
|
||||
subreddit=self.subreddit, page=self.name
|
||||
),
|
||||
**generator_kwargs,
|
||||
)
|
||||
|
||||
@_deprecate_args("content", "reason")
|
||||
def edit(self, *, content: str, reason: str | None = None, **other_settings: Any):
|
||||
"""Edit this wiki page's contents.
|
||||
|
||||
:param content: The updated Markdown content of the page.
|
||||
:param reason: The reason for the revision.
|
||||
:param other_settings: Additional keyword arguments to pass.
|
||||
|
||||
For example, to replace the first wiki page of r/test with the phrase ``"test
|
||||
wiki page"``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
page = next(iter(reddit.subreddit("test").wiki))
|
||||
page.edit(content="test wiki page")
|
||||
|
||||
"""
|
||||
other_settings.update({"content": content, "page": self.name, "reason": reason})
|
||||
self._reddit.post(
|
||||
API_PATH["wiki_edit"].format(subreddit=self.subreddit), data=other_settings
|
||||
)
|
||||
|
||||
def revision(self, revision: str) -> WikiPage:
|
||||
"""Return a specific version of this page by revision ID.
|
||||
|
||||
To view revision ``"1234abc"`` of ``"praw_test"`` in r/test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
page = reddit.subreddit("test").wiki["praw_test"].revision("1234abc")
|
||||
|
||||
"""
|
||||
return WikiPage(self.subreddit._reddit, self.subreddit, self.name, revision)
|
||||
|
||||
def revisions(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Generator[WikiPage, None, None]:
|
||||
"""Return a :class:`.ListingGenerator` for page revisions.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To view the wiki revisions for ``"praw_test"`` in r/test try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.subreddit("test").wiki["praw_test"].revisions():
|
||||
print(item)
|
||||
|
||||
To get :class:`.WikiPage` objects for each revision:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for item in reddit.subreddit("test").wiki["praw_test"].revisions():
|
||||
print(item["page"])
|
||||
|
||||
"""
|
||||
url = API_PATH["wiki_page_revisions"].format(
|
||||
subreddit=self.subreddit, page=self.name
|
||||
)
|
||||
return self._revision_generator(
|
||||
generator_kwargs=generator_kwargs, subreddit=self.subreddit, url=url
|
||||
)
|
||||
114
backend/venv/Lib/site-packages/praw/models/redditors.py
Normal file
114
backend/venv/Lib/site-packages/praw/models/redditors.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""Provide the Redditors class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import islice
|
||||
from types import SimpleNamespace
|
||||
from typing import TYPE_CHECKING, Iterable, Iterator
|
||||
|
||||
import prawcore
|
||||
|
||||
from ..const import API_PATH
|
||||
from .base import PRAWBase
|
||||
from .listing.generator import ListingGenerator
|
||||
from .util import stream_generator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class PartialRedditor(SimpleNamespace):
|
||||
"""A namespace object that provides a subset of :class:`.Redditor` attributes."""
|
||||
|
||||
|
||||
class Redditors(PRAWBase):
|
||||
"""Redditors is a Listing class that provides various :class:`.Redditor` lists."""
|
||||
|
||||
def new(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for new :class:`.Redditors`.
|
||||
|
||||
:returns: :class:`.Redditor` profiles, which are a type of :class:`.Subreddit`.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(self._reddit, API_PATH["users_new"], **generator_kwargs)
|
||||
|
||||
def partial_redditors(self, ids: Iterable[str]) -> Iterator[PartialRedditor]:
|
||||
"""Get user summary data by redditor IDs.
|
||||
|
||||
:param ids: An iterable of redditor fullname IDs.
|
||||
|
||||
:returns: A iterator producing :class:`.PartialRedditor` objects.
|
||||
|
||||
Each ID must be prefixed with ``t2_``.
|
||||
|
||||
Invalid IDs are ignored by the server.
|
||||
|
||||
"""
|
||||
iterable = iter(ids)
|
||||
while True:
|
||||
chunk = list(islice(iterable, 100))
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
params = {"ids": ",".join(chunk)}
|
||||
try:
|
||||
results = self._reddit.get(API_PATH["user_by_fullname"], params=params)
|
||||
except prawcore.exceptions.NotFound:
|
||||
# None of the given IDs matched any Redditor.
|
||||
continue
|
||||
|
||||
for fullname, user_data in results.items():
|
||||
yield PartialRedditor(fullname=fullname, **user_data)
|
||||
|
||||
def popular(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for popular :class:`.Redditors`.
|
||||
|
||||
:returns: :class:`.Redditor` profiles, which are a type of :class:`.Subreddit`.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["users_popular"], **generator_kwargs
|
||||
)
|
||||
|
||||
def search(
|
||||
self, query: str, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
r"""Return a :class:`.ListingGenerator` of Redditors for ``query``.
|
||||
|
||||
:param query: The query string to filter Redditors by.
|
||||
|
||||
:returns: :class:`.Redditor`\ s.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
self._safely_add_arguments(arguments=generator_kwargs, key="params", q=query)
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["users_search"], **generator_kwargs
|
||||
)
|
||||
|
||||
def stream(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Yield new Redditors as they are created.
|
||||
|
||||
Redditors are yielded oldest first. Up to 100 historical Redditors will
|
||||
initially be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
:returns: :class:`.Redditor` profiles, which are a type of :class:`.Subreddit`.
|
||||
|
||||
"""
|
||||
return stream_generator(self.new, **stream_options)
|
||||
18
backend/venv/Lib/site-packages/praw/models/stylesheet.py
Normal file
18
backend/venv/Lib/site-packages/praw/models/stylesheet.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Provide the Stylesheet class."""
|
||||
|
||||
from .base import PRAWBase
|
||||
|
||||
|
||||
class Stylesheet(PRAWBase):
|
||||
"""Represent a stylesheet.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
============== ========================================
|
||||
Attribute Description
|
||||
============== ========================================
|
||||
``images`` A list of images used by the stylesheet.
|
||||
``stylesheet`` The contents of the stylesheet, as CSS.
|
||||
============== ========================================
|
||||
|
||||
"""
|
||||
183
backend/venv/Lib/site-packages/praw/models/subreddits.py
Normal file
183
backend/venv/Lib/site-packages/praw/models/subreddits.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Provide the Subreddits class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Iterator
|
||||
from warnings import warn
|
||||
|
||||
from ..const import API_PATH
|
||||
from ..util import _deprecate_args
|
||||
from . import Subreddit
|
||||
from .base import PRAWBase
|
||||
from .listing.generator import ListingGenerator
|
||||
from .util import stream_generator
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class Subreddits(PRAWBase):
|
||||
"""Subreddits is a Listing class that provides various subreddit lists."""
|
||||
|
||||
@staticmethod
|
||||
def _to_list(subreddit_list: list[str | praw.models.Subreddit]) -> str:
|
||||
return ",".join([str(x) for x in subreddit_list])
|
||||
|
||||
def default(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for default subreddits.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["subreddits_default"], **generator_kwargs
|
||||
)
|
||||
|
||||
def gold(self, **generator_kwargs: Any) -> Iterator[praw.models.Subreddit]:
|
||||
"""Alias for :meth:`.premium` to maintain backwards compatibility."""
|
||||
warn(
|
||||
"'subreddits.gold' has be renamed to 'subreddits.premium'.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.premium(**generator_kwargs)
|
||||
|
||||
def new(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for new subreddits.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["subreddits_new"], **generator_kwargs
|
||||
)
|
||||
|
||||
def popular(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for popular subreddits.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["subreddits_popular"], **generator_kwargs
|
||||
)
|
||||
|
||||
def premium(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` for premium subreddits.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["subreddits_gold"], **generator_kwargs
|
||||
)
|
||||
|
||||
def recommended(
|
||||
self,
|
||||
subreddits: list[str | praw.models.Subreddit],
|
||||
omit_subreddits: list[str | praw.models.Subreddit] | None = None,
|
||||
) -> list[praw.models.Subreddit]:
|
||||
"""Return subreddits recommended for the given list of subreddits.
|
||||
|
||||
:param subreddits: A list of :class:`.Subreddit` instances and/or subreddit
|
||||
names.
|
||||
:param omit_subreddits: A list of :class:`.Subreddit` instances and/or subreddit
|
||||
names to exclude from the results (Reddit's end may not work as expected).
|
||||
|
||||
"""
|
||||
if not isinstance(subreddits, list):
|
||||
msg = "subreddits must be a list"
|
||||
raise TypeError(msg)
|
||||
if omit_subreddits is not None and not isinstance(omit_subreddits, list):
|
||||
msg = "omit_subreddits must be a list or None"
|
||||
raise TypeError(msg)
|
||||
|
||||
params = {"omit": self._to_list(omit_subreddits or [])}
|
||||
url = API_PATH["sub_recommended"].format(subreddits=self._to_list(subreddits))
|
||||
return [
|
||||
Subreddit(self._reddit, sub["sr_name"])
|
||||
for sub in self._reddit.get(url, params=params)
|
||||
]
|
||||
|
||||
def search(
|
||||
self, query: str, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` of subreddits matching ``query``.
|
||||
|
||||
Subreddits are searched by both their title and description.
|
||||
|
||||
:param query: The query string to filter subreddits by.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.search_by_name` to search by subreddit names
|
||||
|
||||
"""
|
||||
self._safely_add_arguments(arguments=generator_kwargs, key="params", q=query)
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["subreddits_search"], **generator_kwargs
|
||||
)
|
||||
|
||||
@_deprecate_args("query", "include_nsfw", "exact")
|
||||
def search_by_name(
|
||||
self,
|
||||
query: str,
|
||||
*,
|
||||
include_nsfw: bool = True,
|
||||
exact: bool = False,
|
||||
) -> list[praw.models.Subreddit]:
|
||||
r"""Return list of :class:`.Subreddit`\ s whose names begin with ``query``.
|
||||
|
||||
:param query: Search for subreddits beginning with this string.
|
||||
:param exact: Return only exact matches to ``query`` (default: ``False``).
|
||||
:param include_nsfw: Include subreddits labeled NSFW (default: ``True``).
|
||||
|
||||
"""
|
||||
result = self._reddit.post(
|
||||
API_PATH["subreddits_name_search"],
|
||||
data={"include_over_18": include_nsfw, "exact": exact, "query": query},
|
||||
)
|
||||
return [self._reddit.subreddit(x) for x in result["names"]]
|
||||
|
||||
def search_by_topic(self, query: str) -> list[praw.models.Subreddit]:
|
||||
"""Return list of Subreddits whose topics match ``query``.
|
||||
|
||||
:param query: Search for subreddits relevant to the search topic.
|
||||
|
||||
.. note::
|
||||
|
||||
As of 09/01/2020, this endpoint always returns 404.
|
||||
|
||||
"""
|
||||
result = self._reddit.get(
|
||||
API_PATH["subreddits_by_topic"], params={"query": query}
|
||||
)
|
||||
return [self._reddit.subreddit(x["name"]) for x in result if x.get("name")]
|
||||
|
||||
def stream(
|
||||
self, **stream_options: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Yield new subreddits as they are created.
|
||||
|
||||
Subreddits are yielded oldest first. Up to 100 historical subreddits will
|
||||
initially be returned.
|
||||
|
||||
Keyword arguments are passed to :func:`.stream_generator`.
|
||||
|
||||
"""
|
||||
return stream_generator(self.new, **stream_options)
|
||||
58
backend/venv/Lib/site-packages/praw/models/trophy.py
Normal file
58
backend/venv/Lib/site-packages/praw/models/trophy.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Represent the :class:`.Trophy` class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .base import PRAWBase
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
|
||||
class Trophy(PRAWBase):
|
||||
"""Represent a trophy.
|
||||
|
||||
End users should not instantiate this class directly. :meth:`.Redditor.trophies` can
|
||||
be used to get a list of the redditor's trophies.
|
||||
|
||||
.. include:: ../../typical_attributes.rst
|
||||
|
||||
=============== ===================================================
|
||||
Attribute Description
|
||||
=============== ===================================================
|
||||
``award_id`` The ID of the trophy (sometimes ``None``).
|
||||
``description`` The description of the trophy (sometimes ``None``).
|
||||
``icon_40`` The URL of a 41x41 px icon for the trophy.
|
||||
``icon_70`` The URL of a 71x71 px icon for the trophy.
|
||||
``name`` The name of the trophy.
|
||||
``url`` A relevant URL (sometimes ``None``).
|
||||
=============== ===================================================
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other: Trophy | Any) -> bool:
|
||||
"""Check if two Trophies are equal."""
|
||||
if isinstance(other, self.__class__):
|
||||
return self.name == other.name
|
||||
return super().__eq__(other)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, _data: dict[str, Any]):
|
||||
"""Initialize a :class:`.Trophy` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
:param _data: The structured data, assumed to be a dict and key ``"name"`` must
|
||||
be provided.
|
||||
|
||||
"""
|
||||
assert isinstance(_data, dict)
|
||||
assert "name" in _data
|
||||
super().__init__(reddit, _data=_data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"{self.__class__.__name__}(name={self.name!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return a name of the trophy."""
|
||||
return self.name
|
||||
293
backend/venv/Lib/site-packages/praw/models/user.py
Normal file
293
backend/venv/Lib/site-packages/praw/models/user.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""Provides the User class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Iterator
|
||||
from warnings import warn
|
||||
|
||||
from prawcore import Conflict
|
||||
|
||||
from ..const import API_PATH
|
||||
from ..exceptions import ReadOnlyException
|
||||
from ..models import Preferences
|
||||
from ..util import _deprecate_args
|
||||
from ..util.cache import cachedproperty
|
||||
from .base import PRAWBase
|
||||
from .listing.generator import ListingGenerator
|
||||
from .reddit.redditor import Redditor
|
||||
from .reddit.subreddit import Subreddit
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw.models
|
||||
|
||||
|
||||
class User(PRAWBase):
|
||||
"""The :class:`.User` class provides methods for the currently authenticated user."""
|
||||
|
||||
@cachedproperty
|
||||
def preferences(self) -> praw.models.Preferences:
|
||||
"""Get an instance of :class:`.Preferences`.
|
||||
|
||||
The preferences can be accessed as a ``dict`` like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
preferences = reddit.user.preferences()
|
||||
print(preferences["show_link_flair"])
|
||||
|
||||
Preferences can be updated via:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reddit.user.preferences.update(show_link_flair=True)
|
||||
|
||||
The :meth:`.Preferences.update` method returns the new state of the preferences
|
||||
as a ``dict``, which can be used to check whether a change went through. Changes
|
||||
with invalid types or parameter names fail silently.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
original_preferences = reddit.user.preferences()
|
||||
new_preferences = reddit.user.preferences.update(invalid_param=123)
|
||||
print(original_preferences == new_preferences) # True, no change
|
||||
|
||||
"""
|
||||
return Preferences(self._reddit)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit):
|
||||
"""Initialize an :class:`.User` instance.
|
||||
|
||||
This class is intended to be interfaced with through ``reddit.user``.
|
||||
|
||||
"""
|
||||
super().__init__(reddit, _data=None)
|
||||
|
||||
def blocked(self) -> list[praw.models.Redditor]:
|
||||
r"""Return a :class:`.RedditorList` of blocked :class:`.Redditor`\ s."""
|
||||
return self._reddit.get(API_PATH["blocked"])
|
||||
|
||||
def contributor_subreddits(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
r"""Return a :class:`.ListingGenerator` of contributor :class:`.Subreddit`\ s.
|
||||
|
||||
These are subreddits in which the user is an approved user.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To print a list of the subreddits that you are an approved user in, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for subreddit in reddit.user.contributor_subreddits(limit=None):
|
||||
print(str(subreddit))
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["my_contributor"], **generator_kwargs
|
||||
)
|
||||
|
||||
@_deprecate_args("user")
|
||||
def friends(
|
||||
self, *, user: str | praw.models.Redditor | None = None
|
||||
) -> list[praw.models.Redditor] | praw.models.Redditor:
|
||||
r"""Return a :class:`.RedditorList` of friends or a :class:`.Redditor` in the friends list.
|
||||
|
||||
:param user: Checks to see if you are friends with the redditor. Either an
|
||||
instance of :class:`.Redditor` or a string can be given.
|
||||
|
||||
:returns: A list of :class:`.Redditor`\ s, or a single :class:`.Redditor` if
|
||||
``user`` is specified. The :class:`.Redditor` instance(s) returned also has
|
||||
friend attributes.
|
||||
|
||||
:raises: An instance of :class:`.RedditAPIException` if you are not friends with
|
||||
the specified :class:`.Redditor`.
|
||||
|
||||
"""
|
||||
endpoint = (
|
||||
API_PATH["friends"]
|
||||
if user is None
|
||||
else API_PATH["friend_v1"].format(user=str(user))
|
||||
)
|
||||
return self._reddit.get(endpoint)
|
||||
|
||||
def karma(self) -> dict[praw.models.Subreddit, dict[str, int]]:
|
||||
r"""Return a dictionary mapping :class:`.Subreddit`\ s to their karma.
|
||||
|
||||
The returned dict contains subreddits as keys. Each subreddit key contains a
|
||||
sub-dict that have keys for ``comment_karma`` and ``link_karma``. The dict is
|
||||
sorted in descending karma order.
|
||||
|
||||
.. note::
|
||||
|
||||
Each key of the main dict is an instance of :class:`.Subreddit`. It is
|
||||
recommended to iterate over the dict in order to retrieve the values,
|
||||
preferably through :py:meth:`dict.items`.
|
||||
|
||||
"""
|
||||
karma_map = {}
|
||||
for row in self._reddit.get(API_PATH["karma"])["data"]:
|
||||
subreddit = Subreddit(self._reddit, row["sr"])
|
||||
del row["sr"]
|
||||
karma_map[subreddit] = row
|
||||
return karma_map
|
||||
|
||||
@_deprecate_args("use_cache")
|
||||
def me(self, *, use_cache: bool = True) -> praw.models.Redditor | None:
|
||||
"""Return a :class:`.Redditor` instance for the authenticated user.
|
||||
|
||||
:param use_cache: When ``True``, and if this function has been previously
|
||||
called, returned the cached version (default: ``True``).
|
||||
|
||||
.. note::
|
||||
|
||||
If you change the :class:`.Reddit` instance's authorization, you might want
|
||||
to refresh the cached value. Prefer using separate :class:`.Reddit`
|
||||
instances, however, for distinct authorizations.
|
||||
|
||||
.. deprecated:: 7.2
|
||||
|
||||
In :attr:`.read_only` mode this method returns ``None``. In PRAW 8 this
|
||||
method will raise :class:`.ReadOnlyException` when called in
|
||||
:attr:`.read_only` mode. To operate in PRAW 8 mode, set the config variable
|
||||
``praw8_raise_exception_on_me`` to ``True``.
|
||||
|
||||
"""
|
||||
if self._reddit.read_only:
|
||||
if not self._reddit.config.custom.get("praw8_raise_exception_on_me"):
|
||||
warn(
|
||||
"The 'None' return value is deprecated, and will raise a"
|
||||
" ReadOnlyException beginning with PRAW 8. See documentation for"
|
||||
" forward compatibility options.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return None
|
||||
msg = "`user.me()` does not work in read_only mode"
|
||||
raise ReadOnlyException(msg)
|
||||
if "_me" not in self.__dict__ or not use_cache:
|
||||
user_data = self._reddit.get(API_PATH["me"])
|
||||
self._me = Redditor(self._reddit, _data=user_data)
|
||||
return self._me
|
||||
|
||||
def moderator_subreddits(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
"""Return a :class:`.ListingGenerator` subreddits that the user moderates.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To print a list of the names of the subreddits you moderate, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for subreddit in reddit.user.moderator_subreddits(limit=None):
|
||||
print(str(subreddit))
|
||||
|
||||
.. seealso::
|
||||
|
||||
:meth:`.Redditor.moderated`
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["my_moderator"], **generator_kwargs
|
||||
)
|
||||
|
||||
def multireddits(self) -> list[praw.models.Multireddit]:
|
||||
r"""Return a list of :class:`.Multireddit`\ s belonging to the user."""
|
||||
return self._reddit.get(API_PATH["my_multireddits"])
|
||||
|
||||
def pin(
|
||||
self, submission: praw.models.Submission, *, num: int = None, state: bool = True
|
||||
) -> praw.models.Submission:
|
||||
"""Set the pin state of a submission on the authenticated user's profile.
|
||||
|
||||
:param submission: An instance of :class:`.Submission` that will be
|
||||
pinned/unpinned.
|
||||
:param num: If specified, the slot in which the submission will be pinned into.
|
||||
If there is a submission already in the specified slot, it will be replaced.
|
||||
If ``None`` or there is not a submission in the specified slot, the first
|
||||
available slot will be used (default: ``None``). If all slots are used the
|
||||
following will occur:
|
||||
|
||||
- Old Reddit:
|
||||
|
||||
1. The submission in the last slot will be unpinned.
|
||||
2. The remaining pinned submissions will be shifted down a slot.
|
||||
3. The new submission will be pinned in the first slot.
|
||||
|
||||
- New Reddit:
|
||||
|
||||
1. The submission in the first slot will be unpinned.
|
||||
2. The remaining pinned submissions will be shifted up a slot.
|
||||
3. The new submission will be pinned in the last slot.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time of writing (10/22/2021), there are 4 pin slots available and
|
||||
pins are in reverse order on old Reddit. If ``num`` is an invalid value,
|
||||
Reddit will ignore it and the same behavior will occur as if ``num`` is
|
||||
``None``.
|
||||
|
||||
:param state: ``True`` pins the submission, ``False`` unpins (default:
|
||||
``True``).
|
||||
|
||||
:returns: The pinned submission.
|
||||
|
||||
:raises: ``prawcore.BadRequest`` when pinning a removed or deleted submission.
|
||||
:raises: ``prawcore.Forbidden`` when pinning a submission the authenticated user
|
||||
is not the author of.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
submission = next(reddit.user.me().submissions.new())
|
||||
reddit.user.pin(submission)
|
||||
|
||||
"""
|
||||
data = {
|
||||
"id": submission.fullname,
|
||||
"num": num,
|
||||
"state": state,
|
||||
"to_profile": True,
|
||||
}
|
||||
try:
|
||||
return self._reddit.post(API_PATH["sticky_submission"], data=data)
|
||||
except Conflict:
|
||||
pass
|
||||
|
||||
def subreddits(
|
||||
self, **generator_kwargs: str | int | dict[str, str]
|
||||
) -> Iterator[praw.models.Subreddit]:
|
||||
r"""Return a :class:`.ListingGenerator` of :class:`.Subreddit`\ s the user is subscribed to.
|
||||
|
||||
Additional keyword arguments are passed in the initialization of
|
||||
:class:`.ListingGenerator`.
|
||||
|
||||
To print a list of the subreddits that you are subscribed to, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
for subreddit in reddit.user.subreddits(limit=None):
|
||||
print(str(subreddit))
|
||||
|
||||
"""
|
||||
return ListingGenerator(
|
||||
self._reddit, API_PATH["my_subreddits"], **generator_kwargs
|
||||
)
|
||||
|
||||
def trusted(self) -> list[praw.models.Redditor]:
|
||||
r"""Return a :class:`.RedditorList` of trusted :class:`.Redditor`\ s.
|
||||
|
||||
To display the usernames of your trusted users and the times at which you
|
||||
decided to trust them, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
trusted_users = reddit.user.trusted()
|
||||
for user in trusted_users:
|
||||
print(f"User: {user.name}, time: {user.date}")
|
||||
|
||||
"""
|
||||
return self._reddit.get(API_PATH["trusted"])
|
||||
230
backend/venv/Lib/site-packages/praw/models/util.py
Normal file
230
backend/venv/Lib/site-packages/praw/models/util.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Provide helper classes used by other models."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Callable, Generator
|
||||
|
||||
from ..util import _deprecate_args
|
||||
|
||||
|
||||
@_deprecate_args("permissions", "known_permissions")
|
||||
def permissions_string(
|
||||
*, known_permissions: set[str], permissions: list[str] | None
|
||||
) -> str:
|
||||
"""Return a comma separated string of permission changes.
|
||||
|
||||
:param known_permissions: A set of strings representing the available permissions.
|
||||
:param permissions: A list of strings, or ``None``. These strings can exclusively
|
||||
contain ``+`` or ``-`` prefixes, or contain no prefixes at all. When prefixed,
|
||||
the resulting string will simply be the joining of these inputs. When not
|
||||
prefixed, all permissions are considered to be additions, and all permissions in
|
||||
the ``known_permissions`` set that aren't provided are considered to be
|
||||
removals. When ``None``, the result is ``"+all"``.
|
||||
|
||||
"""
|
||||
if permissions is None:
|
||||
to_set = ["+all"]
|
||||
else:
|
||||
to_set = ["-all"]
|
||||
omitted = sorted(known_permissions - set(permissions))
|
||||
to_set.extend(f"-{x}" for x in omitted)
|
||||
to_set.extend(f"+{x}" for x in permissions)
|
||||
return ",".join(to_set)
|
||||
|
||||
|
||||
@_deprecate_args(
|
||||
"function",
|
||||
"pause_after",
|
||||
"skip_existing",
|
||||
"attribute_name",
|
||||
"exclude_before",
|
||||
"continue_after_id",
|
||||
)
|
||||
def stream_generator(
|
||||
function: Callable,
|
||||
*,
|
||||
attribute_name: str = "fullname",
|
||||
continue_after_id: str | None = None,
|
||||
exclude_before: bool = False,
|
||||
pause_after: int | None = None,
|
||||
skip_existing: bool = False,
|
||||
**function_kwargs: Any,
|
||||
) -> Generator[Any, None, None]:
|
||||
"""Yield new items from ``function`` as they become available.
|
||||
|
||||
:param function: A callable that returns a :class:`.ListingGenerator`, e.g.,
|
||||
:meth:`.Subreddit.comments` or :meth:`.Subreddit.new`.
|
||||
:param attribute_name: The field to use as an ID (default: ``"fullname"``).
|
||||
:param exclude_before: When ``True`` does not pass ``params`` to ``function``
|
||||
(default: ``False``).
|
||||
:param pause_after: An integer representing the number of requests that result in no
|
||||
new items before this function yields ``None``, effectively introducing a pause
|
||||
into the stream. A negative value yields ``None`` after items from a single
|
||||
response have been yielded, regardless of number of new items obtained in that
|
||||
response. A value of ``0`` yields ``None`` after every response resulting in no
|
||||
new items, and a value of ``None`` never introduces a pause (default: ``None``).
|
||||
:param skip_existing: When ``True``, this does not yield any results from the first
|
||||
request thereby skipping any items that existed in the stream prior to starting
|
||||
the stream (default: ``False``).
|
||||
:param continue_after_id: The initial item ID value to use for ``before`` in
|
||||
``params``. The stream will continue from the item following this one (default:
|
||||
``None``).
|
||||
|
||||
Additional keyword arguments will be passed to ``function``.
|
||||
|
||||
.. note::
|
||||
|
||||
This function internally uses an exponential delay with jitter between
|
||||
subsequent responses that contain no new results, up to a maximum delay of just
|
||||
over 16 seconds. In practice, that means that the time before pause for
|
||||
``pause_after=N+1`` is approximately twice the time before pause for
|
||||
``pause_after=N``.
|
||||
|
||||
For example, to create a stream of comment replies, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
reply_function = reddit.inbox.comment_replies
|
||||
for reply in praw.models.util.stream_generator(reply_function):
|
||||
print(reply)
|
||||
|
||||
To pause a comment stream after six responses with no new comments, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
for comment in subreddit.stream.comments(pause_after=6):
|
||||
if comment is None:
|
||||
break
|
||||
print(comment)
|
||||
|
||||
To resume fetching comments after a pause, try:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
comment_stream = subreddit.stream.comments(pause_after=5)
|
||||
|
||||
for comment in comment_stream:
|
||||
if comment is None:
|
||||
break
|
||||
print(comment)
|
||||
# Do any other processing, then try to fetch more data
|
||||
for comment in comment_stream:
|
||||
if comment is None:
|
||||
break
|
||||
print(comment)
|
||||
|
||||
To bypass the internal exponential backoff, try the following. This approach is
|
||||
useful if you are monitoring a subreddit with infrequent activity, and you want to
|
||||
consistently learn about new items from the stream as soon as possible, rather than
|
||||
up to a delay of just over sixteen seconds.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
subreddit = reddit.subreddit("test")
|
||||
for comment in subreddit.stream.comments(pause_after=0):
|
||||
if comment is None:
|
||||
continue
|
||||
print(comment)
|
||||
|
||||
"""
|
||||
before_attribute = continue_after_id
|
||||
exponential_counter = ExponentialCounter(max_counter=16)
|
||||
seen_attributes = BoundedSet(301)
|
||||
without_before_counter = 0
|
||||
responses_without_new = 0
|
||||
valid_pause_after = pause_after is not None
|
||||
while True:
|
||||
found = False
|
||||
newest_attribute = None
|
||||
limit = 100
|
||||
if before_attribute is None:
|
||||
limit -= without_before_counter
|
||||
without_before_counter = (without_before_counter + 1) % 30
|
||||
if not exclude_before:
|
||||
function_kwargs["params"] = {"before": before_attribute}
|
||||
for item in reversed(list(function(limit=limit, **function_kwargs))):
|
||||
attribute = getattr(item, attribute_name)
|
||||
if attribute in seen_attributes:
|
||||
continue
|
||||
found = True
|
||||
seen_attributes.add(attribute)
|
||||
newest_attribute = attribute
|
||||
if not skip_existing:
|
||||
yield item
|
||||
before_attribute = newest_attribute
|
||||
skip_existing = False
|
||||
if valid_pause_after and pause_after < 0:
|
||||
yield None
|
||||
elif found:
|
||||
exponential_counter.reset()
|
||||
responses_without_new = 0
|
||||
else:
|
||||
responses_without_new += 1
|
||||
if valid_pause_after and responses_without_new > pause_after:
|
||||
exponential_counter.reset()
|
||||
responses_without_new = 0
|
||||
yield None
|
||||
else:
|
||||
time.sleep(exponential_counter.counter())
|
||||
|
||||
|
||||
class BoundedSet:
|
||||
"""A set with a maximum size that evicts the oldest items when necessary.
|
||||
|
||||
This class does not implement the complete set interface.
|
||||
|
||||
"""
|
||||
|
||||
def __contains__(self, item: Any) -> bool:
|
||||
"""Test if the :class:`.BoundedSet` contains item."""
|
||||
self._access(item)
|
||||
return item in self._set
|
||||
|
||||
def __init__(self, max_items: int):
|
||||
"""Initialize a :class:`.BoundedSet` instance."""
|
||||
self.max_items = max_items
|
||||
self._set = OrderedDict()
|
||||
|
||||
def _access(self, item: Any):
|
||||
if item in self._set:
|
||||
self._set.move_to_end(item)
|
||||
|
||||
def add(self, item: Any):
|
||||
"""Add an item to the set discarding the oldest item if necessary."""
|
||||
self._access(item)
|
||||
self._set[item] = None
|
||||
if len(self._set) > self.max_items:
|
||||
self._set.popitem(last=False)
|
||||
|
||||
|
||||
class ExponentialCounter:
|
||||
"""A class to provide an exponential counter with jitter."""
|
||||
|
||||
def __init__(self, max_counter: int):
|
||||
"""Initialize an :class:`.ExponentialCounter` instance.
|
||||
|
||||
:param max_counter: The maximum base value.
|
||||
|
||||
.. note::
|
||||
|
||||
The computed value may be 3.125% higher due to jitter.
|
||||
|
||||
"""
|
||||
self._base = 1
|
||||
self._max = max_counter
|
||||
|
||||
def counter(self) -> int | float:
|
||||
"""Increment the counter and return the current value with jitter."""
|
||||
max_jitter = self._base / 16.0
|
||||
value = self._base + random.random() * max_jitter - max_jitter / 2 # noqa: S311
|
||||
self._base = min(self._base * 2, self._max)
|
||||
return value
|
||||
|
||||
def reset(self):
|
||||
"""Reset the counter to 1."""
|
||||
self._base = 1
|
||||
286
backend/venv/Lib/site-packages/praw/objector.py
Normal file
286
backend/venv/Lib/site-packages/praw/objector.py
Normal file
@@ -0,0 +1,286 @@
|
||||
"""Provides the Objector class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from json import loads
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from .exceptions import ClientException, RedditAPIException
|
||||
from .util import snake_case_keys
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import praw
|
||||
|
||||
from .models.reddit.base import RedditBase
|
||||
|
||||
|
||||
class Objector:
|
||||
"""The objector builds :class:`.RedditBase` objects."""
|
||||
|
||||
@classmethod
|
||||
def check_error(cls, data: list[Any] | dict[str, dict[str, str]]):
|
||||
"""Raise an error if the argument resolves to an error object."""
|
||||
error = cls.parse_error(data)
|
||||
if error:
|
||||
raise error
|
||||
|
||||
@classmethod
|
||||
def parse_error(
|
||||
cls, data: list[Any] | dict[str, dict[str, str]]
|
||||
) -> RedditAPIException | None:
|
||||
"""Convert JSON response into an error object.
|
||||
|
||||
:param data: The dict to be converted.
|
||||
|
||||
:returns: An instance of :class:`.RedditAPIException`, or ``None`` if ``data``
|
||||
doesn't fit this model.
|
||||
|
||||
"""
|
||||
if isinstance(data, list):
|
||||
# Fetching a Submission returns a list (of two items). Although it's handled
|
||||
# manually in `Submission._fetch()`, assume it's a possibility here.
|
||||
return None
|
||||
|
||||
errors = data.get("json", {}).get("errors")
|
||||
if errors is None:
|
||||
return None
|
||||
if len(errors) < 1:
|
||||
# See `Collection._fetch()`.
|
||||
msg = "successful error response"
|
||||
raise ClientException(msg, data)
|
||||
return RedditAPIException(errors)
|
||||
|
||||
def __init__(self, reddit: praw.Reddit, parsers: dict[str, Any] | None = None):
|
||||
"""Initialize an :class:`.Objector` instance.
|
||||
|
||||
:param reddit: An instance of :class:`.Reddit`.
|
||||
|
||||
"""
|
||||
self.parsers = {} if parsers is None else parsers
|
||||
self._reddit = reddit
|
||||
|
||||
def _objectify_dict( # noqa: PLR0912,PLR0915
|
||||
self, data: dict[str, Any]
|
||||
) -> RedditBase:
|
||||
"""Create :class:`.RedditBase` objects from dicts.
|
||||
|
||||
:param data: The structured data, assumed to be a dict.
|
||||
|
||||
:returns: An instance of :class:`.RedditBase`.
|
||||
|
||||
"""
|
||||
if {"messages", "modActions"}.issubset(data) and {
|
||||
"conversations",
|
||||
"conversation",
|
||||
}.intersection(data):
|
||||
# fetched conversation
|
||||
data.update(
|
||||
data.pop("conversation")
|
||||
if "conversation" in data
|
||||
else data.pop("conversations")
|
||||
)
|
||||
parser = self.parsers["ModmailConversation"]
|
||||
parser._convert_conversation_objects(data, self._reddit)
|
||||
elif {"messages", "modActions"}.issubset(data) or {
|
||||
"legacyFirstMessageId",
|
||||
"state",
|
||||
}.issubset(data):
|
||||
# not fetched conversation i.e., from conversations()
|
||||
del data["objIds"] # delete objIds since it could be missing data
|
||||
parser = self.parsers["ModmailConversation"]
|
||||
elif {"conversationIds", "conversations", "messages"}.issubset(data):
|
||||
# modmail conversations
|
||||
conversations = []
|
||||
for conversation_id in data["conversationIds"]:
|
||||
conversation = data["conversations"][conversation_id]
|
||||
# set if the numMessages is same as number of messages in objIds
|
||||
if conversation["numMessages"] == len(
|
||||
[obj for obj in conversation["objIds"] if obj["key"] == "messages"]
|
||||
):
|
||||
conversation["messages"] = [
|
||||
self.objectify(data["messages"][obj_id["id"]])
|
||||
for obj_id in conversation["objIds"]
|
||||
]
|
||||
conversations.append(conversation)
|
||||
data["conversations"] = conversations
|
||||
data = snake_case_keys(data)
|
||||
parser = self.parsers["ModmailConversations-list"]
|
||||
elif {"actionTypeId", "author", "date"}.issubset(data):
|
||||
# Modmail mod action
|
||||
data = snake_case_keys(data)
|
||||
parser = self.parsers["ModmailAction"]
|
||||
elif {"bodyMarkdown", "isInternal"}.issubset(data):
|
||||
# Modmail message
|
||||
data = snake_case_keys(data)
|
||||
parser = self.parsers["ModmailMessage"]
|
||||
elif {"kind", "short_name", "violation_reason"}.issubset(data):
|
||||
# This is a Rule
|
||||
parser = self.parsers["rule"]
|
||||
elif {"isAdmin", "isDeleted"}.issubset(data):
|
||||
# Modmail author
|
||||
data = snake_case_keys(data)
|
||||
# Prevent clobbering base-36 id
|
||||
del data["id"]
|
||||
data["is_subreddit_mod"] = data.pop("is_mod")
|
||||
parser = self.parsers[self._reddit.config.kinds["redditor"]]
|
||||
elif {"banStatus", "muteStatus", "recentComments"}.issubset(data):
|
||||
# Modmail user
|
||||
data = snake_case_keys(data)
|
||||
data["created_string"] = data.pop("created")
|
||||
parser = self.parsers[self._reddit.config.kinds["redditor"]]
|
||||
elif {"displayName", "id", "type"}.issubset(data):
|
||||
# Modmail subreddit
|
||||
data = snake_case_keys(data)
|
||||
parser = self.parsers[self._reddit.config.kinds[data["type"]]]
|
||||
elif {"date", "id", "name"}.issubset(data) or {
|
||||
"id",
|
||||
"name",
|
||||
"permissions",
|
||||
}.issubset(data):
|
||||
parser = self.parsers[self._reddit.config.kinds["redditor"]]
|
||||
elif {"text", "url"}.issubset(data):
|
||||
if "color" in data or "linkUrl" in data:
|
||||
parser = self.parsers["Button"]
|
||||
else:
|
||||
parser = self.parsers["MenuLink"]
|
||||
elif {"children", "text"}.issubset(data):
|
||||
parser = self.parsers["Submenu"]
|
||||
elif {"height", "url", "width"}.issubset(data):
|
||||
parser = self.parsers["Image"]
|
||||
elif {"isSubscribed", "name", "subscribers"}.issubset(data):
|
||||
# discards icon and subscribed information
|
||||
return self._reddit.subreddit(data["name"])
|
||||
elif {"authorFlairType", "name"}.issubset(data):
|
||||
# discards flair information
|
||||
return self._reddit.redditor(data["name"])
|
||||
elif {"parent_id"}.issubset(data):
|
||||
parser = self.parsers[self._reddit.config.kinds["comment"]]
|
||||
elif "collection_id" in data:
|
||||
parser = self.parsers["Collection"]
|
||||
elif {"moderators", "moderatorIds", "allUsersLoaded", "subredditId"}.issubset(
|
||||
data
|
||||
):
|
||||
data = snake_case_keys(data)
|
||||
moderators = []
|
||||
for mod_id in data["moderator_ids"]:
|
||||
mod = snake_case_keys(data["moderators"][mod_id])
|
||||
mod["mod_permissions"] = list(mod["mod_permissions"].keys())
|
||||
moderators.append(mod)
|
||||
data["moderators"] = moderators
|
||||
parser = self.parsers["moderator-list"]
|
||||
elif "username" in data:
|
||||
data["name"] = data.pop("username")
|
||||
parser = self.parsers[self._reddit.config.kinds["redditor"]]
|
||||
elif {"mod_permissions", "name", "sr", "subscribers"}.issubset(data):
|
||||
data["display_name"] = data["sr"]
|
||||
parser = self.parsers[self._reddit.config.kinds["subreddit"]]
|
||||
elif {"drafts", "subreddits"}.issubset(data): # Draft list
|
||||
subreddit_parser = self.parsers[self._reddit.config.kinds["subreddit"]]
|
||||
user_subreddit_parser = self.parsers["UserSubreddit"]
|
||||
subreddits = {
|
||||
subreddit["name"]: (
|
||||
user_subreddit_parser.parse(subreddit, self._reddit)
|
||||
if subreddit["display_name_prefixed"].startswith("u/")
|
||||
else subreddit_parser.parse(subreddit, self._reddit)
|
||||
)
|
||||
for subreddit in data.pop("subreddits")
|
||||
}
|
||||
for draft in data["drafts"]:
|
||||
if draft["subreddit"]:
|
||||
draft["subreddit"] = subreddits[draft["subreddit"]]
|
||||
draft["modified"] = datetime.fromtimestamp(
|
||||
draft["modified"] / 1000
|
||||
).astimezone()
|
||||
parser = self.parsers["DraftList"]
|
||||
elif {"mod_action_data", "user_note_data"}.issubset(data):
|
||||
data["moderator"] = self._reddit.redditor(data["operator"])
|
||||
data["subreddit"] = self._reddit.subreddit(data["subreddit"])
|
||||
data["user"] = self._reddit.redditor(data["user"])
|
||||
# move these sub dict values into the main dict for simplicity
|
||||
data.update(data["mod_action_data"])
|
||||
del data["mod_action_data"]
|
||||
data.update(data["user_note_data"])
|
||||
del data["user_note_data"]
|
||||
parser = self.parsers["mod_note"]
|
||||
elif (
|
||||
"created" in data
|
||||
and isinstance(data["created"], dict)
|
||||
and {"mod_action_data", "user_note_data"}.issubset(data["created"])
|
||||
):
|
||||
data = data["created"]
|
||||
return self._objectify_dict(data)
|
||||
else:
|
||||
if "user" in data:
|
||||
parser = self.parsers[self._reddit.config.kinds["redditor"]]
|
||||
data["user"] = parser.parse({"name": data["user"]}, self._reddit)
|
||||
return data
|
||||
return parser.parse(data, self._reddit)
|
||||
|
||||
def objectify( # noqa: PLR0911,PLR0912,PLR0915
|
||||
self, data: dict[str, Any] | list[Any] | bool | None
|
||||
) -> RedditBase | dict[str, Any] | list[Any] | bool | None:
|
||||
"""Create :class:`.RedditBase` objects from data.
|
||||
|
||||
:param data: The structured data.
|
||||
|
||||
:returns: An instance of :class:`.RedditBase`, or ``None`` if given ``data`` is
|
||||
``None``.
|
||||
|
||||
"""
|
||||
if data is None: # 204 no content
|
||||
return None
|
||||
if isinstance(data, list):
|
||||
return [self.objectify(item) for item in data]
|
||||
if isinstance(data, bool): # Reddit.username_available
|
||||
return data
|
||||
if "json" in data and "errors" in data["json"]:
|
||||
errors = data["json"]["errors"]
|
||||
if len(errors) > 0:
|
||||
raise RedditAPIException(errors)
|
||||
if "kind" in data and (
|
||||
"shortName" in data or data["kind"] in ("menu", "moderators")
|
||||
):
|
||||
# This is a widget
|
||||
parser = self.parsers.get(data["kind"], self.parsers["widget"])
|
||||
return parser.parse(data, self._reddit)
|
||||
if {"kind", "data"}.issubset(data) and data["kind"] in self.parsers:
|
||||
parser = self.parsers[data["kind"]]
|
||||
if data["kind"] == "ModeratedList":
|
||||
return parser.parse(data, self._reddit)
|
||||
return parser.parse(data["data"], self._reddit)
|
||||
if "json" in data and "data" in data["json"]:
|
||||
if "websocket_url" in data["json"]["data"]:
|
||||
return data
|
||||
if "things" in data["json"]["data"]: # Submission.reply
|
||||
return self.objectify(data["json"]["data"]["things"])
|
||||
if "rules" in data["json"]["data"]:
|
||||
return self.objectify(loads(data["json"]["data"]["rules"]))
|
||||
if "drafts_count" in data["json"]["data"] and all(
|
||||
key not in data["json"]["data"] for key in ["name", "url"]
|
||||
): # Draft
|
||||
data["json"]["data"].pop("drafts_count")
|
||||
return self.parsers["Draft"].parse(data["json"]["data"], self._reddit)
|
||||
if "url" in data["json"]["data"]: # Subreddit.submit
|
||||
# The URL is the URL to the submission, so it's removed.
|
||||
del data["json"]["data"]["url"]
|
||||
parser = self.parsers[self._reddit.config.kinds["submission"]]
|
||||
if data["json"]["data"]["id"].startswith(
|
||||
f"{self._reddit.config.kinds['submission']}_"
|
||||
):
|
||||
# With polls, Reddit returns a fullname but calls it an "id". This
|
||||
# fixes this by coercing the fullname into an id.
|
||||
data["json"]["data"]["id"] = data["json"]["data"]["id"].split(
|
||||
"_", 1
|
||||
)[1]
|
||||
else:
|
||||
parser = self.parsers["LiveUpdateEvent"]
|
||||
return parser.parse(data["json"]["data"], self._reddit)
|
||||
if {"is_public_link", "title", "body"}.issubset(data):
|
||||
parser = self.parsers["Draft"]
|
||||
return parser.parse(data, self._reddit)
|
||||
if "rules" in data:
|
||||
return self.objectify(data["rules"])
|
||||
if isinstance(data, dict):
|
||||
return self._objectify_dict(data)
|
||||
return data
|
||||
26
backend/venv/Lib/site-packages/praw/praw.ini
Normal file
26
backend/venv/Lib/site-packages/praw/praw.ini
Normal file
@@ -0,0 +1,26 @@
|
||||
[DEFAULT]
|
||||
# A boolean to indicate whether or not to check for package updates.
|
||||
check_for_updates=True
|
||||
|
||||
# Object to kind mappings
|
||||
comment_kind=t1
|
||||
message_kind=t4
|
||||
redditor_kind=t2
|
||||
submission_kind=t3
|
||||
subreddit_kind=t5
|
||||
trophy_kind=t6
|
||||
|
||||
# The URL prefix for OAuth-related requests.
|
||||
oauth_url=https://oauth.reddit.com
|
||||
|
||||
# The amount of seconds of ratelimit to sleep for upon encountering a specific type of 429 error.
|
||||
ratelimit_seconds=5
|
||||
|
||||
# The URL prefix for regular requests.
|
||||
reddit_url=https://www.reddit.com
|
||||
|
||||
# The URL prefix for short URLs.
|
||||
short_url=https://redd.it
|
||||
|
||||
# The timeout for requests to Reddit in number of seconds
|
||||
timeout=16
|
||||
1019
backend/venv/Lib/site-packages/praw/reddit.py
Normal file
1019
backend/venv/Lib/site-packages/praw/reddit.py
Normal file
File diff suppressed because it is too large
Load Diff
5
backend/venv/Lib/site-packages/praw/util/__init__.py
Normal file
5
backend/venv/Lib/site-packages/praw/util/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Package imports for utilities."""
|
||||
|
||||
from .cache import cachedproperty
|
||||
from .deprecate_args import _deprecate_args
|
||||
from .snake import camel_to_snake, snake_case_keys
|
||||
52
backend/venv/Lib/site-packages/praw/util/cache.py
Normal file
52
backend/venv/Lib/site-packages/praw/util/cache.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Caching utilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
class cachedproperty: # noqa: N801
|
||||
"""A decorator for caching a property's result.
|
||||
|
||||
Similar to :py:class:`property`, but the wrapped method's result is cached on the
|
||||
instance. This is achieved by setting an entry in the object's instance dictionary
|
||||
with the same name as the property. When the name is later accessed, the value in
|
||||
the instance dictionary takes precedence over the (non-data descriptor) property.
|
||||
|
||||
This is useful for implementing lazy-loaded properties.
|
||||
|
||||
The cache can be invalidated via :py:meth:`delattr`, or by modifying ``__dict__``
|
||||
directly. It will be repopulated on next access.
|
||||
|
||||
.. versionadded:: 6.3.0
|
||||
|
||||
"""
|
||||
|
||||
# This to make sphinx run properly
|
||||
def __call__(self, *args: Any, **kwargs: Any): # pragma: no cover
|
||||
"""Empty method to make sphinx run properly."""
|
||||
|
||||
def __get__(self, obj: Any | None, objtype: Any | None = None) -> Any:
|
||||
"""Implement descriptor getter.
|
||||
|
||||
Calculate the property's value and then store it in the associated object's
|
||||
instance dictionary.
|
||||
|
||||
"""
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
value = obj.__dict__[self.func.__name__] = self.func(obj)
|
||||
return value
|
||||
|
||||
def __init__(self, func: Callable[[Any], Any], doc: str | None = None):
|
||||
"""Initialize a :class:`.cachedproperty` instance."""
|
||||
self.func = self.__wrapped__ = func
|
||||
|
||||
if doc is None:
|
||||
doc = func.__doc__
|
||||
self.__doc__ = doc
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Return an object initialization representation of the instance."""
|
||||
return f"<{self.__class__.__name__} {self.func}>"
|
||||
50
backend/venv/Lib/site-packages/praw/util/deprecate_args.py
Normal file
50
backend/venv/Lib/site-packages/praw/util/deprecate_args.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Positional argument deprecation decorator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
from warnings import warn
|
||||
|
||||
|
||||
def _deprecate_args(*old_args: str) -> Callable:
|
||||
def _generate_arg_string(used_args: tuple[str, ...]) -> str:
|
||||
used_args = list(map(repr, used_args))
|
||||
arg_count = len(used_args)
|
||||
arg_string = (
|
||||
" and ".join(used_args)
|
||||
if arg_count < 3
|
||||
else f"{', '.join(used_args[:-1])}, and {used_args[-1]}"
|
||||
)
|
||||
arg_string += f" as {'' if arg_count > 1 else 'a '}"
|
||||
arg_string += "keyword argument"
|
||||
return arg_string + ("s" if arg_count > 1 else "")
|
||||
|
||||
def wrapper(func: Callable):
|
||||
@wraps(func)
|
||||
def wrapped(*args: Any, **kwargs: Any):
|
||||
signature = inspect.signature(func)
|
||||
positional_args = [
|
||||
name
|
||||
for name, parameter in signature.parameters.items()
|
||||
if parameter.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
|
||||
]
|
||||
_old_args = tuple(filter(lambda arg: arg not in positional_args, old_args))
|
||||
if positional_args:
|
||||
# remove the acceptable positional arguments like self or id for helpers
|
||||
kwargs.update(zip(positional_args, args))
|
||||
args = tuple(args[len(positional_args) :])
|
||||
if args:
|
||||
arg_string = _generate_arg_string(_old_args[: len(args)])
|
||||
warn(
|
||||
f"Positional arguments for {func.__qualname__!r} will no longer be"
|
||||
f" supported in PRAW 8.\nCall this function with {arg_string}.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return func(**dict(zip(_old_args, args)), **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
return wrapper
|
||||
22
backend/venv/Lib/site-packages/praw/util/snake.py
Normal file
22
backend/venv/Lib/site-packages/praw/util/snake.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""Contains functions dealing with snake case conversions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_re_camel_to_snake = re.compile(r"([a-z0-9](?=[A-Z])|[A-Z](?=[A-Z][a-z]))")
|
||||
|
||||
|
||||
def camel_to_snake(name: str) -> str:
|
||||
"""Convert ``name`` from camelCase to snake_case."""
|
||||
return _re_camel_to_snake.sub(r"\1_", name).lower()
|
||||
|
||||
|
||||
def snake_case_keys(dictionary: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return a new dictionary with keys converted to snake_case.
|
||||
|
||||
:param dictionary: The dict to be corrected.
|
||||
|
||||
"""
|
||||
return {camel_to_snake(k): v for k, v in dictionary.items()}
|
||||
203
backend/venv/Lib/site-packages/praw/util/token_manager.py
Normal file
203
backend/venv/Lib/site-packages/praw/util/token_manager.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Token Manager classes.
|
||||
|
||||
There should be a 1-to-1 mapping between an instance of a subclass of
|
||||
:class:`.BaseTokenManager` and a :class:`.Reddit` instance.
|
||||
|
||||
A few proof of concept token manager classes are provided here, but it is expected that
|
||||
PRAW users will create their own token manager classes suitable for their needs.
|
||||
|
||||
.. deprecated:: 7.4.0
|
||||
|
||||
Tokens managers have been deprecated and will be removed in the near future.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import _deprecate_args
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import prawcore
|
||||
|
||||
import praw
|
||||
|
||||
|
||||
class BaseTokenManager(ABC):
|
||||
"""An abstract class for all token managers."""
|
||||
|
||||
@abstractmethod
|
||||
def post_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Handle callback that is invoked after a refresh token is used.
|
||||
|
||||
:param authorizer: The ``prawcore.Authorizer`` instance used containing
|
||||
``access_token`` and ``refresh_token`` attributes.
|
||||
|
||||
This function will be called after refreshing the access and refresh tokens.
|
||||
This callback can be used for saving the updated ``refresh_token``.
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def pre_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Handle callback that is invoked before refreshing PRAW's authorization.
|
||||
|
||||
:param authorizer: The ``prawcore.Authorizer`` instance used containing
|
||||
``access_token`` and ``refresh_token`` attributes.
|
||||
|
||||
This callback can be used to inspect and modify the attributes of the
|
||||
``prawcore.Authorizer`` instance, such as setting the ``refresh_token``.
|
||||
|
||||
"""
|
||||
|
||||
@property
|
||||
def reddit(self) -> praw.Reddit:
|
||||
"""Return the :class:`.Reddit` instance bound to the token manager."""
|
||||
return self._reddit
|
||||
|
||||
@reddit.setter
|
||||
def reddit(self, value: praw.Reddit):
|
||||
if self._reddit is not None:
|
||||
msg = "'reddit' can only be set once and is done automatically"
|
||||
raise RuntimeError(msg)
|
||||
self._reddit = value
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize a :class:`.BaseTokenManager` instance."""
|
||||
self._reddit = None
|
||||
|
||||
|
||||
class FileTokenManager(BaseTokenManager):
|
||||
"""Provides a single-file based token manager.
|
||||
|
||||
It is expected that the file with the initial ``refresh_token`` is created prior to
|
||||
use.
|
||||
|
||||
.. warning::
|
||||
|
||||
The same ``file`` should not be used by more than one instance of this class
|
||||
concurrently. Doing so may result in data corruption. Consider using
|
||||
:class:`.SQLiteTokenManager` if you want more than one instance of PRAW to
|
||||
concurrently manage a specific ``refresh_token`` chain.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str):
|
||||
"""Initialize a :class:`.FileTokenManager` instance.
|
||||
|
||||
:param filename: The file the contains the refresh token.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
self._filename = filename
|
||||
|
||||
def post_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Update the saved copy of the refresh token."""
|
||||
with Path(self._filename).open("w") as fp:
|
||||
fp.write(authorizer.refresh_token)
|
||||
|
||||
def pre_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Load the refresh token from the file."""
|
||||
if authorizer.refresh_token is None:
|
||||
with Path(self._filename).open() as fp:
|
||||
authorizer.refresh_token = fp.read().strip()
|
||||
|
||||
|
||||
class SQLiteTokenManager(BaseTokenManager):
|
||||
"""Provides a SQLite3 based token manager.
|
||||
|
||||
Unlike, :class:`.FileTokenManager`, the initial database need not be created ahead
|
||||
of time, as it'll automatically be created on first use. However, initial refresh
|
||||
tokens will need to be registered via :meth:`.register` prior to use.
|
||||
|
||||
.. warning::
|
||||
|
||||
This class is untested on Windows because we encountered file locking issues in
|
||||
the test environment.
|
||||
|
||||
"""
|
||||
|
||||
@_deprecate_args("database", "key")
|
||||
def __init__(self, *, database: str, key: str):
|
||||
"""Initialize a :class:`.SQLiteTokenManager` instance.
|
||||
|
||||
:param database: The path to the SQLite database.
|
||||
:param key: The key used to locate the refresh token. This ``key`` can be
|
||||
anything. You might use the ``client_id`` if you expect to have unique a
|
||||
refresh token for each ``client_id``, or you might use a redditor's
|
||||
``username`` if you're managing multiple users' authentications.
|
||||
|
||||
"""
|
||||
super().__init__()
|
||||
import sqlite3
|
||||
|
||||
self._connection = sqlite3.connect(database)
|
||||
self._connection.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tokens (id, refresh_token, updated_at)"
|
||||
)
|
||||
self._connection.execute(
|
||||
"CREATE UNIQUE INDEX IF NOT EXISTS ux_tokens_id on tokens(id)"
|
||||
)
|
||||
self._connection.commit()
|
||||
self.key = key
|
||||
|
||||
def _get(self):
|
||||
cursor = self._connection.execute(
|
||||
"SELECT refresh_token FROM tokens WHERE id=?", (self.key,)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
if result is None:
|
||||
raise KeyError
|
||||
return result[0]
|
||||
|
||||
def _set(self, refresh_token: str):
|
||||
"""Set the refresh token in the database.
|
||||
|
||||
This function will overwrite an existing value if the corresponding ``key``
|
||||
already exists.
|
||||
|
||||
"""
|
||||
self._connection.execute(
|
||||
"REPLACE INTO tokens VALUES (?, ?, datetime('now'))",
|
||||
(self.key, refresh_token),
|
||||
)
|
||||
self._connection.commit()
|
||||
|
||||
def is_registered(self) -> bool:
|
||||
"""Return whether ``key`` already has a ``refresh_token``."""
|
||||
cursor = self._connection.execute(
|
||||
"SELECT refresh_token FROM tokens WHERE id=?", (self.key,)
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
def post_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Update the refresh token in the database."""
|
||||
self._set(authorizer.refresh_token)
|
||||
|
||||
# While the following line is not strictly necessary, it ensures that the
|
||||
# refresh token is not used elsewhere. And also forces the pre_refresh_callback
|
||||
# to always load the latest refresh_token from the database.
|
||||
authorizer.refresh_token = None
|
||||
|
||||
def pre_refresh_callback(self, authorizer: prawcore.auth.BaseAuthorizer):
|
||||
"""Load the refresh token from the database."""
|
||||
assert authorizer.refresh_token is None
|
||||
authorizer.refresh_token = self._get()
|
||||
|
||||
def register(self, refresh_token: str) -> bool:
|
||||
"""Register the initial refresh token in the database.
|
||||
|
||||
:returns: ``True`` if ``refresh_token`` is saved to the database, otherwise,
|
||||
``False`` if there is already a ``refresh_token`` for the associated
|
||||
``key``.
|
||||
|
||||
"""
|
||||
cursor = self._connection.execute(
|
||||
"INSERT OR IGNORE INTO tokens VALUES (?, ?, datetime('now'))",
|
||||
(self.key, refresh_token),
|
||||
)
|
||||
self._connection.commit()
|
||||
return cursor.rowcount == 1
|
||||
Reference in New Issue
Block a user