Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import functools
2import sys
3from pathlib import Path
4from string import Formatter
5from typing import Any, Callable, Dict, Iterator, List, Optional, Set
7import fire
8import praw
9import toml
10from praw.exceptions import MissingRequiredAttributeException
11from praw.models import Submission
12from praw.models.reddit.subreddit import Subreddit
14from reddit_get.types import SortingOption, TimeFilterOption
17class RedditCli:
18 """Get content from reddit.
20 This is intended to be a suite of command line tools that will allow
21 you to get content from Reddit. Currently this is limited to getting
22 the titles of posts. Use `reddit-get post --help` for more
23 information.
25 Note, In order to use this tool, you must supply your reddit
26 credentials and api credentials in a file in the following format:
28 [reddit-get]
29 client_id = "testid"
30 client_secret = "testsecret"
31 user_agent = "testuseragent"
32 username = "testusername"
33 password = "testpassword"
35 Args:
36 config: The path on your system for your reddit credentials.
37 Required. Default $HOME/.redditgetrc
38 """
40 def __init__(self, config: str = '~/.redditgetrc'):
41 self.config_path: Path = Path(config).expanduser()
42 try:
43 self.configs = toml.load(self.config_path)
44 except (FileNotFoundError, toml.TomlDecodeError):
45 raise fire.core.FireError(f'No valid TOML config found at {self.config_path}')
46 try:
47 self.reddit = praw.Reddit(**self.configs['reddit-get'])
48 except MissingRequiredAttributeException as e: # pragma: no cover
49 fire.core.FireError(e)
50 if not self.reddit.user.me():
51 raise fire.core.FireError( # pragma: no cover
52 'Failed to authenticate with Reddit. Did you remember your username and password?'
53 )
54 self.valid_header_variables: Dict[str, Dict[Optional[SortingOption, TimeFilterOption], str]] = {
55 'sorting': {
56 SortingOption.CONTROVERSIAL: 'Most Controversial',
57 SortingOption.GILDED: 'Most Awarded',
58 SortingOption.HOT: 'Hottest',
59 SortingOption.NEW: 'Newest',
60 SortingOption.RANDOM_RISING: 'Randomly Selected Rising',
61 SortingOption.RISING: 'Rising',
62 SortingOption.TOP: 'Top',
63 },
64 'time_filter': {
65 TimeFilterOption.HOUR: 'the Past Hour',
66 TimeFilterOption.DAY: 'the Last Day',
67 TimeFilterOption.WEEK: 'the Last Week',
68 TimeFilterOption.MONTH: 'Last Month',
69 TimeFilterOption.YEAR: 'Last Year',
70 TimeFilterOption.ALL: 'All Time',
71 },
72 }
74 def config_location(self):
75 """Get the path of the reddit-get config.
77 Returns: The path to the config file in use for reddit-get
78 """
79 if self.config_path:
80 return self.config_path.resolve()
81 else:
82 raise fire.core.FireError(f'No config_path has been set!')
84 def _create_header(
85 self, template: str, sorting: SortingOption, time: TimeFilterOption, subreddit: str
86 ) -> str:
87 valid_keys = {'sorting', 'time', 'subreddit'}
88 keys = self._get_template_keys(template)
89 if not keys.issubset(valid_keys):
90 raise fire.core.FireError(
91 f'Invalid keys passed into header template: {", ".join(keys - valid_keys)}'
92 )
93 format_params = {
94 'sorting': self.valid_header_variables['sorting'][sorting],
95 'time': self.valid_header_variables['time_filter'][time],
96 'subreddit': f'r/{subreddit}',
97 }
98 return template.format(**format_params)
100 def _create_post_output(self, template: str, posts: Iterator[Submission]) -> List[str]:
101 template_vars = self._get_template_keys(template)
102 results = []
103 for post in posts:
104 try:
105 format_params = {key: getattr(post, key) for key in template_vars}
106 results.append(template.format(**format_params))
107 except AttributeError as e:
108 raise fire.core.FireError(e)
109 return results
111 @staticmethod
112 def _get_template_keys(template: str) -> Set[str]:
113 template_vars = {tup[1] for tup in Formatter().parse(template) if tup[1]}
114 return template_vars
116 def post(
117 self,
118 subreddit: str,
119 post_sorting: str = 'top',
120 time_filter: str = 'all',
121 limit: int = 10,
122 header: bool = True,
123 custom_header: str = '#### The {sorting} Posts for {time} from {subreddit}',
124 output_format: str = '- {title}',
125 ) -> List[str]:
126 """Get Reddit post titles optionally formatted as markdown.
128 This is a handy script for someone who is looking to get reddit
129 post titles returned in a markdown format. For example, I use
130 this to get the daily or weekly news, a daily quote, and some
131 shower thoughts formatted as markdown from Reddit for my
132 Obsidian daily tracker.
134 Args:
135 subreddit: Which subreddit to get posts from
136 post_sorting: How to sort the posts, choose from
137 'controversial', 'gilded', 'hot', 'new', 'random_rising',
138 'rising', or 'top'
139 time_filter: For 'controversial' or 'top' post sorting,
140 choose the date range between 'hour', 'day', 'week',
141 'month', 'year', or 'all'
142 limit: Limit of the number of posts to get, default 10,
143 limit 25
144 header: Whether or not to include a header in the result.
145 Default is true, use --noheader if you do not want a header.
146 custom_header: Template to use for a custom header for the
147 response. You can use one of 3 special keywords: 'sorting',
148 'time', and 'subreddit' which should be wrapped in curly
149 braces. For example, you could pass something like this:
151 "--> Here are the {sorting} posts from {subreddit} for {time} <--"
153 and the header would be this if you are using 'hot' for post
154 sorting and 'week' for time_filter and showerthoughts for
155 the subreddit:
157 "--> Here Are the Hottest Posts From R/Showerthoughts for Last Week <--"
159 (Note the title casing).
160 output_format: The template for the output of each post. As
161 with custom_header, wrap any items you want to include in
162 curly braces. You can include any items from the [Praw
163 Subreddit Model](http://lira.no-ip.org:8080/doc/praw-doc/html/code_overview/models/subreddit
164 .html#subreddit). Hint: You can include emojis and things
165 like newlines ("\n"), tabs("\t"), and anything else. Example
167 "Title - {title} 🤪\nText - {selftext}\n👍🏻"
169 This might have some output like this:
171 Title - What do sprinters eat before a race? 🤪
172 Text - Nothing, they fast
173 👍
175 Returns:
176 The number of post titles from the specified subreddit
177 formatted as specified
178 """
179 try:
180 post_sorting = SortingOption(post_sorting)
181 except ValueError:
182 raise fire.core.FireError(f'{post_sorting} is not a valid sorting option.')
183 try:
184 time_filter = TimeFilterOption(time_filter)
185 except ValueError:
186 raise fire.core.FireError(f'{time_filter} is not a valid time filter option')
187 if not 0 < limit <= 25:
188 raise fire.core.FireError('You may only get between 1 and 25 submissions')
190 praw_subreddit: Subreddit = self.reddit.subreddit(subreddit)
192 call_map: Dict[SortingOption, Callable[[Optional[int]], Iterator[Any]]] = {
193 SortingOption.CONTROVERSIAL: functools.partial(
194 praw_subreddit.controversial, time_filter=time_filter
195 ),
196 SortingOption.GILDED: praw_subreddit.gilded,
197 SortingOption.HOT: praw_subreddit.hot,
198 SortingOption.NEW: praw_subreddit.new,
199 SortingOption.RANDOM_RISING: praw_subreddit.random_rising,
200 SortingOption.RISING: praw_subreddit.rising,
201 SortingOption.TOP: functools.partial(praw_subreddit.top, time_filter=time_filter),
202 }
204 response_header = (
205 [
206 self._create_header(
207 template=custom_header, sorting=post_sorting, time=time_filter, subreddit=subreddit
208 )
209 ]
210 if header
211 else []
212 )
214 posts: List[str] = self._create_post_output(output_format, call_map[post_sorting](limit=limit))
216 return response_header + posts
219def main(): # pragma: no cover
220 try:
221 fire.Fire(RedditCli)
222 except fire.core.FireError as e:
223 print(e, file=sys.stderr)
224 sys.exit(255)