Hide keyboard shortcuts

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 

6 

7import fire 

8import praw 

9import toml 

10from praw.exceptions import MissingRequiredAttributeException 

11from praw.models import Submission 

12from praw.models.reddit.subreddit import Subreddit 

13 

14from reddit_get.types import SortingOption, TimeFilterOption 

15 

16 

17class RedditCli: 

18 """Get content from reddit. 

19 

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. 

24 

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: 

27 

28 [reddit-get] 

29 client_id = "testid" 

30 client_secret = "testsecret" 

31 user_agent = "testuseragent" 

32 username = "testusername" 

33 password = "testpassword" 

34 

35 Args: 

36 config: The path on your system for your reddit credentials. 

37 Required. Default $HOME/.redditgetrc 

38 """ 

39 

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 } 

73 

74 def config_location(self): 

75 """Get the path of the reddit-get config. 

76 

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!') 

83 

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) 

99 

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 

110 

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 

115 

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. 

127 

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. 

133 

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: 

150 

151 "--> Here are the {sorting} posts from {subreddit} for {time} <--" 

152 

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: 

156 

157 "--> Here Are the Hottest Posts From R/Showerthoughts for Last Week <--" 

158 

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 

166 

167 "Title - {title} 🤪\nText - {selftext}\n👍🏻" 

168 

169 This might have some output like this: 

170 

171 Title - What do sprinters eat before a race? 🤪 

172 Text - Nothing, they fast 

173 👍 

174 

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') 

189 

190 praw_subreddit: Subreddit = self.reddit.subreddit(subreddit) 

191 

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 } 

203 

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 ) 

213 

214 posts: List[str] = self._create_post_output(output_format, call_map[post_sorting](limit=limit)) 

215 

216 return response_header + posts 

217 

218 

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)