class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() self.r = Reddit(USER_AGENT, site_name='reddit_oauth_test', disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url('...').split('?', 1) self.assertTrue('api/v1/authorize/' in url) params = dict(x.split('=', 1) for x in params.split('&')) expected = {'client_id': self.r.config.client_id, 'duration': 'temporary', 'redirect_uri': ('https%3A%2F%2F127.0.0.1%3A65010%2F' 'authorize_callback'), 'response_type': 'code', 'scope': 'identity', 'state': '...'} self.assertEqual(expected, params) @betamax() def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information('MQALrr1di8GzcnT8szbTWhLcBUQ') expected = {'access_token': self.r.access_token, 'refresh_token': None, 'scope': set(('identity',))} self.assertEqual(expected, token) self.assertEqual('PyAPITestUser2', text_type(self.r.user)) @betamax() def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, 'invalid_code') def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, 'dummy_code') def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') @betamax() def test_invalid_set_access_credentials(self): self.assertRaises(errors.OAuthInvalidToken, self.r.set_access_credentials, set(('identity',)), 'dummy_access_token') def test_oauth_scope_required(self): self.r.set_oauth_app_info('dummy_client', 'dummy_secret', 'dummy_url') self.r.set_access_credentials(set('dummy_scope',), 'dummy_token') self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) @betamax() def test_scope_edit(self): self.r.refresh_access_information(self.refresh_token['edit']) submission = Submission.from_id(self.r, self.submission_edit_id) self.assertEqual(submission, submission.edit('Edited text')) @betamax() def test_scope_history(self): self.r.refresh_access_information(self.refresh_token['history']) self.assertTrue(list(self.r.get_redditor(self.un).get_upvoted())) @betamax() def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token['identity']) self.assertEqual(self.un, self.r.get_me().name) @betamax() def test_scope_modconfig(self): self.r.refresh_access_information(self.refresh_token['modconfig']) self.r.get_subreddit(self.sr).set_settings('foobar') retval = self.r.get_subreddit(self.sr).get_stylesheet() self.assertTrue('images' in retval) @betamax() def test_scope_modflair(self): self.r.refresh_access_information(self.refresh_token['modflair']) self.r.get_subreddit(self.sr).set_flair(self.un, 'foobar') @betamax() def test_scope_modlog(self): num = 50 self.r.refresh_access_information(self.refresh_token['modlog']) result = self.r.get_subreddit(self.sr).get_mod_log(limit=num) self.assertEqual(num, len(list(result))) @betamax() def test_scope_modothers_modself(self): subreddit = self.r.get_subreddit(self.sr) self.r.refresh_access_information(self.refresh_token['modothers']) subreddit.add_moderator(self.other_user_name) # log in as other user self.r.refresh_access_information(self.other_refresh_token['modself']) self.r.accept_moderator_invite(self.sr) # now return to original user. self.r.refresh_access_information(self.refresh_token['modothers']) subreddit.remove_moderator(self.other_user_name) @betamax() def test_scope_modposts(self): self.r.refresh_access_information(self.refresh_token['modposts']) Submission.from_id(self.r, self.submission_edit_id).remove() @betamax() def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) self.assertTrue(list(self.r.get_my_moderation())) @betamax() def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information( self.refresh_token['creddits']) redditor = self.r.get_redditor('bboe') sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, '0', '-1'): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax() def test_scope_privatemessages(self): self.r.refresh_access_information( self.refresh_token['privatemessages']) self.assertTrue(list(self.r.get_inbox())) @betamax() def test_scope_read(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = '{0}_{1}'.format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax() def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token['read']) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax() def test_scope_read_get_sub_listingr(self): self.r.refresh_access_information(self.refresh_token['read']) subreddit = self.r.get_subreddit(self.priv_sr) self.assertTrue(list(subreddit.get_top())) @betamax() def test_scope_read_get_submission_by_url(self): url = ("https://www.reddit.com/r/reddit_api_test_priv/comments/16kbb7/" "google/") self.r.refresh_access_information(self.refresh_token['read']) submission = Submission.from_url(self.r, url) self.assertTrue(submission.num_comments != 0) @betamax() def test_scope_read_priv_sr_comments(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(list(self.r.get_comments(self.priv_sr))) @betamax() def test_scope_wikiread_wiki_page(self): self.r.refresh_access_information(self.refresh_token['wikiread']) self.assertTrue(self.r.get_wiki_page(self.sr, 'index')) @betamax() def test_scope_read_priv_sub_comments(self): self.r.refresh_access_information(self.refresh_token['read']) submission = Submission.from_id(self.r, self.priv_submission_id) self.assertTrue(submission.comments) @betamax() def test_scope_submit(self): self.r.refresh_access_information(self.refresh_token['submit']) result = self.r.submit(self.sr, 'OAuth Submit', text='Foo') self.assertTrue(isinstance(result, Submission)) @betamax() def test_scope_subscribe(self): self.r.refresh_access_information(self.refresh_token['subscribe']) self.r.get_subreddit(self.sr).subscribe() @betamax() def test_scope_vote(self): self.r.refresh_access_information(self.refresh_token['vote']) submission = Submission.from_id(self.r, self.submission_edit_id) submission.clear_vote() @betamax() def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token['edit']) self.assertTrue(self.r.user is None)
class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() self.r = Reddit(USER_AGENT, site_name='reddit_oauth_test', disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url('...').split('?', 1) self.assertTrue('api/v1/authorize/' in url) params = dict(x.split('=', 1) for x in params.split('&')) expected = { 'client_id': self.r.config.client_id, 'duration': 'temporary', 'redirect_uri': ('https%3A%2F%2F127.0.0.1%3A65010%2F' 'authorize_callback'), 'response_type': 'code', 'scope': 'identity', 'state': '...' } self.assertEqual(expected, params) @betamax() @mock_sys_stream("stdin") def test_empty_captcha_file(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) self.assertRaises(errors.InvalidCaptcha, self.r.submit, self.sr, 'captcha test will fail', 'body') @betamax() def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information('MQALrr1di8GzcnT8szbTWhLcBUQ') expected = { 'access_token': self.r.access_token, 'refresh_token': None, 'scope': set(('identity', )) } self.assertEqual(expected, token) self.assertEqual('PyAPITestUser2', text_type(self.r.user)) @betamax() def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, 'invalid_code') @betamax() @mock_sys_stream("stdin") def test_inject_captcha_into_kwargs_and_raise(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) # praw doesn't currently add the captcha into kwargs so lets # write a function in which it would and alias it to Reddit.submit @decorators.restrict_access(scope='submit') @decorators.require_captcha def submit_alias(r, sr, title, text, **kw): return self.r.submit.__wrapped__.__wrapped__( r, sr, title, text, captcha=kw.get('captcha')) self.assertRaises(errors.InvalidCaptcha, submit_alias, self.r, self.sr, 'captcha test will fail', 'body') def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, 'dummy_code') def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') @betamax() def test_invalid_set_access_credentials(self): self.assertRaises(errors.OAuthInvalidToken, self.r.set_access_credentials, set( ('identity', )), 'dummy_access_token') def test_oauth_scope_required(self): self.r.set_oauth_app_info('dummy_client', 'dummy_secret', 'dummy_url') self.r.set_access_credentials(set('dummy_scope', ), 'dummy_token') self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) def test_raise_client_exception(self): def raise_client_exception(*args): raise errors.ClientException(*args) self.assertRaises(errors.ClientException, raise_client_exception) self.assertRaises(errors.ClientException, raise_client_exception, 'test') ce_message = errors.ClientException('Test') ce_no_message = errors.ClientException() self.assertEqual(ce_message.message, str(ce_message)) self.assertEqual(ce_no_message.message, str(ce_no_message)) def test_raise_http_exception(self): def raise_http_exception(): raise errors.HTTPException('fakeraw') self.assertRaises(errors.HTTPException, raise_http_exception) http_exception = errors.HTTPException('fakeraw') self.assertEqual(http_exception.message, str(http_exception)) def test_raise_oauth_exception(self): oerrormessage = "fakemessage" oerrorurl = "http://oauth.reddit.com/" def raise_oauth_exception(): raise errors.OAuthException(oerrormessage, oerrorurl) self.assertRaises(errors.OAuthException, raise_oauth_exception) oauth_exception = errors.OAuthException(oerrormessage, oerrorurl) self.assertEqual( oauth_exception.message + " on url {0}".format(oauth_exception.url), str(oauth_exception)) def test_raise_redirect_exception(self): apiurl = "http://api.reddit.com/" oauthurl = "http://oauth.reddit.com/" def raise_redirect_exception(): raise errors.RedirectException(apiurl, oauthurl) self.assertRaises(errors.RedirectException, raise_redirect_exception) redirect_exception = errors.RedirectException(apiurl, oauthurl) self.assertEqual(redirect_exception.message, str(redirect_exception)) @betamax() def test_scope_history(self): self.r.refresh_access_information(self.refresh_token['history']) self.assertTrue(list(self.r.get_redditor(self.un).get_upvoted())) @betamax() def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token['identity']) self.assertEqual(self.un, self.r.get_me().name) @betamax() def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) self.assertTrue(list(self.r.get_my_moderation())) @betamax() def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information(self.refresh_token['creddits']) redditor = self.r.get_redditor('bboe') sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, '0', '-1', 37, '37'): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax() def test_scope_privatemessages(self): self.r.refresh_access_information( self.refresh_token['privatemessages']) self.assertTrue(list(self.r.get_inbox())) @betamax() def test_scope_read(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = '{0}_{1}'.format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax() def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token['read']) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax() def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_set_access_credentials_with_list(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) result['scope'] = list(result['scope']) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_set_access_credentials_with_string(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) result['scope'] = ' '.join(result['scope']) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() @mock_sys_stream("stdin", "ljgtoo") def test_solve_captcha(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) self.r.submit(self.sr, 'captcha test', 'body') @betamax() @mock_sys_stream("stdin", "DFIRSW") def test_solve_captcha_on_bound_subreddit(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) subreddit = self.r.get_subreddit(self.sr) # praw doesn't currently have a function in which require_captcha # gets a reddit instance from a subreddit and uses it, so lets # write a function in which it would and alias it to Reddit.submit @decorators.restrict_access(scope='submit') @decorators.require_captcha def submit_alias(sr, title, text, **kw): return self.r.submit.__wrapped__.__wrapped__( self.r, sr, title, text, captcha=kw.get('captcha')) submit_alias(subreddit, 'captcha test on bound subreddit', 'body') @betamax() def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token['edit']) self.assertTrue(self.r.user is None)
class SubRedditStats(object): """Contain all the functionality of the subreddit_stats command.""" post_prefix = tt('Subreddit Stats:') post_header = tt('---\n###{0}\n') post_footer = tt('>Generated with [BBoe](/u/bboe)\'s [Subreddit Stats]' '(https://github.com/praw-dev/prawtools) \n{0}' 'SRS Marker: {1}') re_marker = re.compile(r'SRS Marker: (\d+)') @staticmethod def _previous_max(submission): try: val = SubRedditStats.re_marker.findall(submission.selftext)[-1] return float(val) except (IndexError, TypeError): print('End marker not found in previous submission. Aborting') sys.exit(1) @staticmethod def _permalink(permalink): tokens = permalink.split('/') if tokens[8] == '': # submission return tt('/comments/{0}/_/').format(tokens[6]) else: # comment return tt('/comments/{0}/_/{1}?context=1').format(tokens[6], tokens[8]) @staticmethod def _pts(points): return '1 pt' if points == 1 else '{0} pts'.format(points) @staticmethod def _user(user): if user is None: return '_deleted_' elif isinstance(user, Redditor): user = str(user) return tt('[{0}](/user/{1})').format(user.replace('_', r'\_'), user) @staticmethod def _submit(func, *args, **kwargs): def sleep(sleep_time): print('\tSleeping for {0} seconds'.format(sleep_time)) time.sleep(sleep_time) while True: try: return func(*args, **kwargs) except RateLimitExceeded as error: sleep(error.sleep_time) except ExceptionList as exception_list: for error in exception_list.errors: if isinstance(error, RateLimitExceeded): sleep(error.sleep_time) break else: raise def __init__(self, subreddit, site, verbosity, distinguished): """Initialize the SubRedditStats instance with config options.""" self.reddit = Reddit(str(self), site, disable_update_check=True) self.subreddit = self.reddit.get_subreddit(subreddit) self.verbosity = verbosity self.distinguished = distinguished self.submissions = [] self.comments = [] self.submitters = defaultdict(list) self.commenters = defaultdict(list) self.min_date = 0 self.max_date = time.time() - DAYS_IN_SECONDS * 3 self.prev_srs = None def login(self, user, pswd): """Login and provide debugging output if so wanted.""" if self.verbosity > 0: print('Logging in') self.reddit.login(user, pswd) def msg(self, msg, level, overwrite=False): """Output a messaage to the screen if the verbosity is sufficient.""" if self.verbosity and self.verbosity >= level: sys.stdout.write(msg) if overwrite: sys.stdout.write('\r') sys.stdout.flush() else: sys.stdout.write('\n') def prev_stat(self, prev_url): """Load the previous subreddit stats page.""" submission = self.reddit.get_submission(prev_url) self.min_date = self._previous_max(submission) self.prev_srs = prev_url def fetch_recent_submissions(self, max_duration, after, exclude_self, exclude_link, since_last=True): """Fetch recent submissions in subreddit with boundaries. Does not include posts within the last three days as their scores may not be representative. :param max_duration: When set, specifies the number of days to include :param after: When set, fetch all submission after this submission id. :param exclude_self: When true, don't include self posts. :param exclude_link: When true, don't include links. :param since_last: When true use info from last submission to determine the stop point :returns: True if any submissions were found. """ if exclude_self and exclude_link: raise TypeError('Cannot set both exclude_self and exclude_link.') if max_duration: self.min_date = self.max_date - DAYS_IN_SECONDS * max_duration params = {'after': after} if after else None self.msg('DEBUG: Fetching submissions', 1) for submission in self.subreddit.get_new(limit=None, params=params): if submission.created_utc > self.max_date: continue if submission.created_utc <= self.min_date: break if since_last and str(submission.author) == str(self.reddit.user) \ and submission.title.startswith(self.post_prefix): # Use info in this post to update the min_date # And don't include this post self.msg(tt('Found previous: {0}') .format(safe_title(submission)), 2) if self.prev_srs is None: # Only use the most recent self.min_date = max(self.min_date, self._previous_max(submission)) self.prev_srs = submission.permalink continue if exclude_self and submission.is_self: continue if exclude_link and not submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def fetch_top_submissions(self, top, exclude_self, exclude_link): """Fetch top 1000 submissions by some top value. :param top: One of week, month, year, all :param exclude_self: When true, don't include self posts. :param exclude_link: When true, include only self posts :returns: True if any submissions were found. """ if exclude_self and exclude_link: raise TypeError('Cannot set both exclude_self and exclude_link.') if top not in ('day', 'week', 'month', 'year', 'all'): raise TypeError('{0!r} is not a valid top value'.format(top)) self.msg('DEBUG: Fetching submissions', 1) params = {'t': top} for submission in self.subreddit.get_top(limit=None, params=params): if exclude_self and submission.is_self: continue if exclude_link and not submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def process_submitters(self): """Group submissions by author.""" self.msg('DEBUG: Processing Submitters', 1) for submission in self.submissions: if submission.author and (self.distinguished or submission.distinguished is None): self.submitters[str(submission.author)].append(submission) def process_commenters(self): """Group comments by author.""" num = len(self.submissions) self.msg('DEBUG: Processing Commenters on {0} submissions'.format(num), 1) for i, submission in enumerate(self.submissions): # Explicitly fetch as many comments as possible by top sort # Note that this is the first time the complete submission object # is obtained. Only a partial object was returned when getting the # subreddit listings. try: submission = self.reddit.get_submission(submission.permalink, comment_limit=None, comment_sort='top') except HTTPError as exc: print('Ignoring comments on {0} due to HTTP status {1}' .format(submission.url, exc.response.status_code)) continue self.msg('{0}/{1} submissions'.format(i + 1, num), 2, overwrite=True) if submission.num_comments == 0: continue skipped = submission.replace_more_comments() if skipped: skip_num = sum(x.count for x in skipped) print('Ignored {0} comments ({1} MoreComment objects)' .format(skip_num, len(skipped))) comments = [x for x in flatten_tree(submission.comments) if self.distinguished or x.distinguished is None] self.comments.extend(comments) # pylint: disable=W0212 for orphans in itervalues(submission._orphaned): self.comments.extend(orphans) # pylint: enable=W0212 for comment in self.comments: if comment.author: self.commenters[str(comment.author)].append(comment) def basic_stats(self): """Return a markdown representation of simple statistics.""" sub_score = sum(x.score for x in self.submissions) comm_score = sum(x.score for x in self.comments) sub_duration = self.max_date - self.min_date sub_rate = (86400. * len(self.submissions) / sub_duration if sub_duration else len(self.submissions)) # Compute comment rate if self.comments: self.comments.sort(key=lambda x: x.created_utc) duration = (self.comments[-1].created_utc - self.comments[0].created_utc) comm_rate = (86400. * len(self.comments) / duration if duration else len(self.comments)) else: comm_rate = 0 values = [('Total', len(self.submissions), len(self.comments)), ('Rate (per day)', '{0:.2f}'.format(sub_rate), '{0:.2f}'.format(comm_rate)), ('Unique Redditors', len(self.submitters), len(self.commenters)), ('Combined Score', sub_score, comm_score)] retval = 'Period: {0:.2f} days\n\n'.format(sub_duration / 86400.) retval += '||Submissions|Comments|\n:-:|--:|--:\n' for quad in values: # pylint: disable=W0142 retval += '__{0}__|{1}|{2}\n'.format(*quad) # pylint: enable=W0142 return retval + '\n' def top_submitters(self, num, num_submissions): """Return a markdown representation of the top submitters.""" num = min(num, len(self.submitters)) if num <= 0: return '' top_submitters = sorted(iteritems(self.submitters), reverse=True, key=lambda x: (sum(y.score for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Submitters\' Top Submissions') for (author, submissions) in top_submitters: retval += '0. {0}, {1} submission{2}: {3}\n'.format( self._pts(sum(x.score for x in submissions)), len(submissions), 's' if len(submissions) > 1 else '', self._user(author)) for sub in sorted(submissions, reverse=True, key=lambda x: x.score)[:num_submissions]: title = safe_title(sub) if sub.permalink != sub.url: retval += tt(' 0. [{0}]({1})').format(title, sub.url) else: retval += tt(' 0. {0}').format(title) retval += ' ({0}, [{1} comment{2}]({3}))\n'.format( self._pts(sub.score), sub.num_comments, 's' if sub.num_comments > 1 else '', self._permalink(sub.permalink)) retval += '\n' return retval def top_commenters(self, num): """Return a markdown representation of the top commenters.""" score = lambda x: x.score num = min(num, len(self.commenters)) if num <= 0: return '' top_commenters = sorted(iteritems(self.commenters), reverse=True, key=lambda x: (sum(score(y) for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Commenters') for author, comments in top_commenters: retval += '0. {0} ({1}, {2} comment{3})\n'.format( self._user(author), self._pts(sum(score(x) for x in comments)), len(comments), 's' if len(comments) > 1 else '') return '{0}\n'.format(retval) def top_submissions(self, num): """Return a markdown representation of the top submissions.""" num = min(num, len(self.submissions)) if num <= 0: return '' top_submissions = sorted( [x for x in self.submissions if self.distinguished or x.distinguished is None], reverse=True, key=lambda x: x.score)[:num] if not top_submissions: return '' retval = self.post_header.format('Top Submissions') for sub in top_submissions: title = safe_title(sub) if sub.permalink != sub.url: retval += tt('0. [{0}]({1})').format(title, sub.url) else: retval += tt('0. {0}').format(title) retval += ' by {0} ({1}, [{2} comment{3}]({4}))\n'.format( self._user(sub.author), self._pts(sub.score), sub.num_comments, 's' if sub.num_comments > 1 else '', self._permalink(sub.permalink)) return tt('{0}\n').format(retval) def top_comments(self, num): """Return a markdown representation of the top comments.""" score = lambda x: x.score num = min(num, len(self.comments)) if num <= 0: return '' top_comments = sorted(self.comments, reverse=True, key=score)[:num] retval = self.post_header.format('Top Comments') for comment in top_comments: title = safe_title(comment.submission) retval += tt('0. {0}: {1}\'s [comment]({2}) in {3}\n').format( self._pts(score(comment)), self._user(comment.author), self._permalink(comment.permalink), title) return tt('{0}\n').format(retval) def publish_results(self, subreddit, submitters, commenters, submissions, comments, top, debug=False): """Submit the results to the subreddit. Has no return value (None).""" def timef(timestamp, date_only=False): """Return a suitable string representaation of the timestamp.""" dtime = datetime.fromtimestamp(timestamp) if date_only: retval = dtime.strftime('%Y-%m-%d') else: retval = dtime.strftime('%Y-%m-%d %H:%M PDT') return retval if self.prev_srs: prev = '[Prev SRS]({0}) \n'.format(self._permalink(self.prev_srs)) else: prev = '' basic = self.basic_stats() t_commenters = self.top_commenters(commenters) t_submissions = self.top_submissions(submissions) t_comments = self.top_comments(comments) footer = self.post_footer.format(prev, self.max_date) body = '' num_submissions = 10 while body == '' or len(body) > MAX_BODY_SIZE and num_submissions > 2: t_submitters = self.top_submitters(submitters, num_submissions) body = (basic + t_submitters + t_commenters + t_submissions + t_comments + footer) num_submissions -= 1 if len(body) > MAX_BODY_SIZE: print('The resulting message is too big. Not submitting.') debug = True # Set the initial title base_title = '{0} {1} {2}posts from {3} to {4}'.format( self.post_prefix, str(self.subreddit), 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) submitted = False while not debug and not submitted: if subreddit: # Verify the user wants to submit to the subreddit msg = ('You are about to submit to subreddit {0!r} as {1!r}.\n' 'Are you sure? yes/[no]: ' .format(subreddit, str(self.reddit.user))) sys.stdout.write(msg) sys.stdout.flush() if sys.stdin.readline().strip().lower() not in ['y', 'yes']: subreddit = None elif not subreddit: # Prompt for the subreddit to submit to msg = ('Please enter a subreddit to submit to (press return to' ' abort): ') sys.stdout.write(msg) sys.stdout.flush() subreddit = sys.stdin.readline().strip() if not subreddit: print('Submission aborted\n') debug = True # Vary the title depending on where posting if str(self.subreddit) == subreddit: title = '{0} {1}posts from {2} to {3}'.format( self.post_prefix, 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) else: title = base_title if subreddit: # Attempt to make the submission try: res = self._submit(self.reddit.submit, subreddit, title, text=body) print(res.permalink) submitted = True except Exception as error: # pylint: disable=W0703 print('The submission failed:' + str(error)) subreddit = None if not submitted: print(base_title) print(body) def save_csv(self, filename): """Create csv file containing comments and submissions by author.""" redditors = set(self.submitters.keys()).union(self.commenters.keys()) mapping = dict((x.lower(), x) for x in redditors) with codecs.open(filename, 'w', encoding='utf-8') as outfile: outfile.write('username, type, permalink, score\n') for _, redditor in sorted(mapping.items()): for submission in self.submitters.get(redditor, []): outfile.write(u'{0}, submission, {1}, {2}\n' .format(redditor, submission.permalink, submission.score)) for comment in self.commenters.get(redditor, []): outfile.write(u'{0}, comment, {1}, {2}\n' .format(redditor, comment.permalink, comment.score))
class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() self.r = Reddit(USER_AGENT, site_name='reddit_oauth_test', disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url('...').split('?', 1) self.assertTrue('api/v1/authorize/' in url) params = dict(x.split('=', 1) for x in params.split('&')) expected = { 'client_id': self.r.config.client_id, 'duration': 'temporary', 'redirect_uri': ('https%3A%2F%2F127.0.0.1%3A65010%2F' 'authorize_callback'), 'response_type': 'code', 'scope': 'identity', 'state': '...' } self.assertEqual(expected, params) # @betamax() is currently broken for this test because the cassettes # are caching too aggressively and not performing a token refresh. def test_auto_refresh_token(self): self.r.refresh_access_information(self.refresh_token['identity']) old_token = self.r.access_token self.r.access_token += 'x' # break the token self.r.user.refresh() current_token = self.r.access_token self.assertNotEqual(old_token, current_token) self.r.user.refresh() self.assertEqual(current_token, self.r.access_token) @betamax() def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information('MQALrr1di8GzcnT8szbTWhLcBUQ') expected = { 'access_token': self.r.access_token, 'refresh_token': None, 'scope': set(('identity', )) } self.assertEqual(expected, token) self.assertEqual('PyAPITestUser2', text_type(self.r.user)) @betamax() def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, 'invalid_code') def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, 'dummy_code') def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') @betamax() def test_invalid_set_access_credentials(self): self.assertRaises(errors.OAuthInvalidToken, self.r.set_access_credentials, set( ('identity', )), 'dummy_access_token') def test_oauth_scope_required(self): self.r.set_oauth_app_info('dummy_client', 'dummy_secret', 'dummy_url') self.r.set_access_credentials(set('dummy_scope', ), 'dummy_token') self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) def test_raise_client_exception(self): def raise_client_exception(*args): raise errors.ClientException(*args) self.assertRaises(errors.ClientException, raise_client_exception) self.assertRaises(errors.ClientException, raise_client_exception, 'test') ce_message = errors.ClientException('Test') ce_no_message = errors.ClientException() self.assertEqual(ce_message.message, str(ce_message)) self.assertEqual(ce_no_message.message, str(ce_no_message)) def test_raise_http_exception(self): def raise_http_exception(): raise errors.HTTPException('fakeraw') self.assertRaises(errors.HTTPException, raise_http_exception) http_exception = errors.HTTPException('fakeraw') self.assertEqual(http_exception.message, str(http_exception)) def test_raise_oauth_exception(self): oerrormessage = "fakemessage" oerrorurl = "http://oauth.reddit.com/" def raise_oauth_exception(): raise errors.OAuthException(oerrormessage, oerrorurl) self.assertRaises(errors.OAuthException, raise_oauth_exception) oauth_exception = errors.OAuthException(oerrormessage, oerrorurl) self.assertEqual( oauth_exception.message + " on url {0}".format(oauth_exception.url), str(oauth_exception)) def test_raise_redirect_exception(self): apiurl = "http://api.reddit.com/" oauthurl = "http://oauth.reddit.com/" def raise_redirect_exception(): raise errors.RedirectException(apiurl, oauthurl) self.assertRaises(errors.RedirectException, raise_redirect_exception) redirect_exception = errors.RedirectException(apiurl, oauthurl) self.assertEqual(redirect_exception.message, str(redirect_exception)) @betamax() def test_scope_history(self): self.r.refresh_access_information(self.refresh_token['history']) self.assertTrue(list(self.r.get_redditor(self.un).get_upvoted())) @betamax() def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token['identity']) self.assertEqual(self.un, self.r.get_me().name) @betamax() def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) self.assertTrue(list(self.r.get_my_moderation())) @betamax() def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information(self.refresh_token['creddits']) redditor = self.r.get_redditor('bboe') sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, '0', '-1'): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax() def test_scope_privatemessages(self): self.r.refresh_access_information( self.refresh_token['privatemessages']) self.assertTrue(list(self.r.get_inbox())) @betamax() def test_scope_read(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = '{0}_{1}'.format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax() def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token['read']) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax() def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_solve_captcha(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) original_stdin = sys.stdin sys.stdin = FakeStdin('ljgtoo') # Comment this line when rebuilding self.r.submit(self.sr, 'captcha test', 'body') sys.stdin = original_stdin @betamax() def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token['edit']) self.assertTrue(self.r.user is None)
class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() self.r = Reddit(USER_AGENT, site_name='reddit_oauth_test', disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url('...').split('?', 1) self.assertTrue('api/v1/authorize/' in url) params = dict(x.split('=', 1) for x in params.split('&')) expected = {'client_id': self.r.config.client_id, 'duration': 'temporary', 'redirect_uri': ('https%3A%2F%2F127.0.0.1%3A65010%2F' 'authorize_callback'), 'response_type': 'code', 'scope': 'identity', 'state': '...'} self.assertEqual(expected, params) # @betamax() is currently broken for this test because the cassettes # are caching too aggressively and not performing a token refresh. def test_auto_refresh_token(self): self.r.refresh_access_information(self.refresh_token['identity']) old_token = self.r.access_token self.r.access_token += 'x' # break the token self.r.user.refresh() current_token = self.r.access_token self.assertNotEqual(old_token, current_token) self.r.user.refresh() self.assertEqual(current_token, self.r.access_token) @betamax() def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information('MQALrr1di8GzcnT8szbTWhLcBUQ') expected = {'access_token': self.r.access_token, 'refresh_token': None, 'scope': set(('identity',))} self.assertEqual(expected, token) self.assertEqual('PyAPITestUser2', text_type(self.r.user)) @betamax() def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, 'invalid_code') def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, 'dummy_code') def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') @betamax() def test_invalid_set_access_credentials(self): self.assertRaises(errors.OAuthInvalidToken, self.r.set_access_credentials, set(('identity',)), 'dummy_access_token') def test_oauth_scope_required(self): self.r.set_oauth_app_info('dummy_client', 'dummy_secret', 'dummy_url') self.r.set_access_credentials(set('dummy_scope',), 'dummy_token') self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) def test_raise_client_exception(self): def raise_client_exception(*args): raise errors.ClientException(*args) self.assertRaises(errors.ClientException, raise_client_exception) self.assertRaises(errors.ClientException, raise_client_exception, 'test') ce_message = errors.ClientException('Test') ce_no_message = errors.ClientException() self.assertEqual(ce_message.message, str(ce_message)) self.assertEqual(ce_no_message.message, str(ce_no_message)) def test_raise_http_exception(self): def raise_http_exception(): raise errors.HTTPException('fakeraw') self.assertRaises(errors.HTTPException, raise_http_exception) http_exception = errors.HTTPException('fakeraw') self.assertEqual(http_exception.message, str(http_exception)) def test_raise_oauth_exception(self): oerrormessage = "fakemessage" oerrorurl = "http://oauth.reddit.com/" def raise_oauth_exception(): raise errors.OAuthException(oerrormessage, oerrorurl) self.assertRaises(errors.OAuthException, raise_oauth_exception) oauth_exception = errors.OAuthException(oerrormessage, oerrorurl) self.assertEqual(oauth_exception.message + " on url {0}".format(oauth_exception.url), str(oauth_exception)) def test_raise_redirect_exception(self): apiurl = "http://api.reddit.com/" oauthurl = "http://oauth.reddit.com/" def raise_redirect_exception(): raise errors.RedirectException(apiurl, oauthurl) self.assertRaises(errors.RedirectException, raise_redirect_exception) redirect_exception = errors.RedirectException(apiurl, oauthurl) self.assertEqual(redirect_exception.message, str(redirect_exception)) @betamax() def test_scope_history(self): self.r.refresh_access_information(self.refresh_token['history']) self.assertTrue(list(self.r.get_redditor(self.un).get_upvoted())) @betamax() def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token['identity']) self.assertEqual(self.un, self.r.get_me().name) @betamax() def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) self.assertTrue(list(self.r.get_my_moderation())) @betamax() def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information( self.refresh_token['creddits']) redditor = self.r.get_redditor('bboe') sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, '0', '-1'): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax() def test_scope_privatemessages(self): self.r.refresh_access_information( self.refresh_token['privatemessages']) self.assertTrue(list(self.r.get_inbox())) @betamax() def test_scope_read(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = '{0}_{1}'.format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax() def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token['read']) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax() def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_solve_captcha(self): # Use the alternate account because it has low karma, # so we can test the captcha. self.r.refresh_access_information(self.other_refresh_token['submit']) original_stdin = sys.stdin sys.stdin = FakeStdin('ljgtoo') # Comment this line when rebuilding self.r.submit(self.sr, 'captcha test', 'body') sys.stdin = original_stdin @betamax() def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token['edit']) self.assertTrue(self.r.user is None)
class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() site_name = (os.getenv('REDDIT_SITE') or 'reddit') + '_oauth_test' self.r = Reddit(USER_AGENT, site_name=site_name, disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url('...').split('?', 1) self.assertTrue('api/v1/authorize/' in url) params = dict(x.split('=', 1) for x in params.split('&')) expected = { 'client_id': self.r.config.client_id, 'duration': 'temporary', 'redirect_uri': ('https%3A%2F%2F127.0.0.1%3A65010%2F' 'authorize_callback'), 'response_type': 'code', 'scope': 'identity', 'state': '...' } self.assertEqual(expected, params) @betamax def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information('MQALrr1di8GzcnT8szbTWhLcBUQ') expected = { 'access_token': self.r.access_token, 'refresh_token': None, 'scope': set(('identity', )) } self.assertEqual(expected, token) self.assertEqual('PyAPITestUser2', text_type(self.r.user)) @betamax def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, 'invalid_code') def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, 'dummy_code') def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, 'dummy_state') @betamax def test_invalid_set_access_credentials(self): self.assertRaises(errors.OAuthInvalidToken, self.r.set_access_credentials, set( ('identity', )), 'dummy_access_token') def test_oauth_scope_required(self): self.r.set_oauth_app_info('dummy_client', 'dummy_secret', 'dummy_url') self.r.set_access_credentials(set('dummy_scope', ), 'dummy_token') self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) @betamax def test_scope_edit(self): self.r.refresh_access_information(self.refresh_token['edit']) submission = Submission.from_id(self.r, self.submission_edit_id) self.assertEqual(submission, submission.edit('Edited text')) @betamax def test_scope_history(self): self.r.refresh_access_information(self.refresh_token['history']) self.assertTrue(list(self.r.get_redditor(self.un).get_liked())) @betamax def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token['identity']) self.assertEqual(self.un, self.r.get_me().name) @betamax def test_scope_modconfig(self): self.r.refresh_access_information(self.refresh_token['modconfig']) self.r.get_subreddit(self.sr).set_settings('foobar') retval = self.r.get_subreddit(self.sr).get_stylesheet() self.assertTrue('images' in retval) @betamax def test_scope_modflair(self): self.r.refresh_access_information(self.refresh_token['modflair']) self.r.get_subreddit(self.sr).set_flair(self.un, 'foobar') @betamax def test_scope_modlog(self): num = 50 self.r.refresh_access_information(self.refresh_token['modlog']) result = self.r.get_subreddit(self.sr).get_mod_log(limit=num) self.assertEqual(num, len(list(result))) @betamax def test_scope_modposts(self): self.r.refresh_access_information(self.refresh_token['modposts']) Submission.from_id(self.r, self.submission_edit_id).remove() @betamax def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) self.assertTrue(list(self.r.get_my_moderation())) @betamax def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information(self.refresh_token['creddits']) redditor = self.r.get_redditor('bboe') sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, '0', '-1'): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax def test_scope_privatemessages(self): self.r.refresh_access_information( self.refresh_token['privatemessages']) self.assertTrue(list(self.r.get_inbox())) @betamax def test_scope_read(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = '{0}_{1}'.format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token['mysubreddits']) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token['read']) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax def test_scope_read_get_sub_listingr(self): self.r.refresh_access_information(self.refresh_token['read']) subreddit = self.r.get_subreddit(self.priv_sr) self.assertTrue(list(subreddit.get_top())) @betamax def test_scope_read_get_submission_by_url(self): url = ("https://www.reddit.com/r/reddit_api_test_priv/comments/16kbb7/" "google/") self.r.refresh_access_information(self.refresh_token['read']) submission = Submission.from_url(self.r, url) self.assertTrue(submission.num_comments != 0) @betamax def test_scope_read_priv_sr_comments(self): self.r.refresh_access_information(self.refresh_token['read']) self.assertTrue(list(self.r.get_comments(self.priv_sr))) @betamax def test_scope_read_priv_sub_comments(self): self.r.refresh_access_information(self.refresh_token['read']) submission = Submission.from_id(self.r, self.priv_submission_id) self.assertTrue(submission.comments) @betamax def test_scope_submit(self): self.r.refresh_access_information(self.refresh_token['submit']) result = self.r.submit(self.sr, 'OAuth Submit', text='Foo') self.assertTrue(isinstance(result, Submission)) @betamax def test_scope_subscribe(self): self.r.refresh_access_information(self.refresh_token['subscribe']) self.r.get_subreddit(self.sr).subscribe() @betamax def test_scope_vote(self): self.r.refresh_access_information(self.refresh_token['vote']) submission = Submission.from_id(self.r, self.submission_edit_id) submission.clear_vote() @betamax def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information( self.refresh_token['identity'], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token['edit']) self.assertTrue(self.r.user is None)
class OAuth2RedditTest(PRAWTest): def setUp(self): self.configure() self.r = Reddit(USER_AGENT, site_name="reddit_oauth_test", disable_update_check=True) def test_authorize_url(self): self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, "dummy_state") self.r.set_oauth_app_info(self.r.config.client_id, self.r.config.client_secret, self.r.config.redirect_uri) url, params = self.r.get_authorize_url("...").split("?", 1) self.assertTrue("api/v1/authorize/" in url) params = dict(x.split("=", 1) for x in params.split("&")) expected = { "client_id": self.r.config.client_id, "duration": "temporary", "redirect_uri": ("https%3A%2F%2F127.0.0.1%3A65010%2F" "authorize_callback"), "response_type": "code", "scope": "identity", "state": "...", } self.assertEqual(expected, params) # @betamax() is currently broken for this test def test_auto_refresh_token(self): self.r.refresh_access_information(self.refresh_token["identity"]) old_token = self.r.access_token self.r.access_token += "x" # break the token self.r.user.refresh() current_token = self.r.access_token self.assertNotEqual(old_token, current_token) self.r.user.refresh() self.assertEqual(current_token, self.r.access_token) @betamax() def test_get_access_information(self): # If this test fails, the following URL will need to be visted in order # to obtain a new code to pass to `get_access_information`: # self.r.get_authorize_url('...') token = self.r.get_access_information("MQALrr1di8GzcnT8szbTWhLcBUQ") expected = {"access_token": self.r.access_token, "refresh_token": None, "scope": set(("identity",))} self.assertEqual(expected, token) self.assertEqual("PyAPITestUser2", text_type(self.r.user)) @betamax() def test_get_access_information_with_invalid_code(self): self.assertRaises(errors.OAuthInvalidGrant, self.r.get_access_information, "invalid_code") def test_invalid_app_access_token(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_access_information, "dummy_code") def test_invalid_app_authorize_url(self): self.r.clear_authentication() self.r.set_oauth_app_info(None, None, None) self.assertRaises(errors.OAuthAppRequired, self.r.get_authorize_url, "dummy_state") @betamax() def test_invalid_set_access_credentials(self): self.assertRaises( errors.OAuthInvalidToken, self.r.set_access_credentials, set(("identity",)), "dummy_access_token" ) def test_oauth_scope_required(self): self.r.set_oauth_app_info("dummy_client", "dummy_secret", "dummy_url") self.r.set_access_credentials(set("dummy_scope"), "dummy_token") self.assertRaises(errors.OAuthScopeRequired, self.r.get_me) @betamax() def test_scope_edit(self): self.r.refresh_access_information(self.refresh_token["edit"]) submission = Submission.from_id(self.r, self.submission_edit_id) self.assertEqual(submission, submission.edit("Edited text")) @betamax() def test_scope_history(self): self.r.refresh_access_information(self.refresh_token["history"]) self.assertTrue(list(self.r.get_redditor(self.un).get_upvoted())) @betamax() def test_scope_identity(self): self.r.refresh_access_information(self.refresh_token["identity"]) self.assertEqual(self.un, self.r.get_me().name) @betamax() def test_scope_modconfig(self): self.r.refresh_access_information(self.refresh_token["modconfig"]) self.r.get_subreddit(self.sr).set_settings("foobar") retval = self.r.get_subreddit(self.sr).get_stylesheet() self.assertTrue("images" in retval) @betamax() def test_scope_modflair(self): self.r.refresh_access_information(self.refresh_token["modflair"]) self.r.get_subreddit(self.sr).set_flair(self.un, "foobar") @betamax() def test_scope_modlog(self): num = 50 self.r.refresh_access_information(self.refresh_token["modlog"]) result = self.r.get_subreddit(self.sr).get_mod_log(limit=num) self.assertEqual(num, len(list(result))) @betamax() def test_scope_modothers_modself(self): subreddit = self.r.get_subreddit(self.sr) self.r.refresh_access_information(self.refresh_token["modothers"]) subreddit.add_moderator(self.other_user_name) # log in as other user self.r.refresh_access_information(self.other_refresh_token["modself"]) self.r.accept_moderator_invite(self.sr) # now return to original user. self.r.refresh_access_information(self.refresh_token["modothers"]) subreddit.remove_moderator(self.other_user_name) @betamax() def test_scope_modposts(self): self.r.refresh_access_information(self.refresh_token["modposts"]) Submission.from_id(self.r, self.submission_edit_id).remove() @betamax() def test_scope_modself(self): subreddit = self.r.get_subreddit(self.sr) self.r.refresh_access_information(self.refresh_token["modothers"]) subreddit.add_moderator(self.other_user_name) self.r.refresh_access_information(self.refresh_token["modcontributors"]) subreddit.add_contributor(self.other_user_name) # log in as other user self.r.refresh_access_information(self.other_refresh_token["modself"]) self.r.accept_moderator_invite(self.sr) self.r.leave_moderator(subreddit) subreddit.leave_contributor() subreddit.refresh() self.assertFalse(subreddit.user_is_moderator) self.assertFalse(subreddit.user_is_contributor) @betamax() def test_scope_mysubreddits(self): self.r.refresh_access_information(self.refresh_token["mysubreddits"]) self.assertTrue(list(self.r.get_my_moderation())) @betamax() def test_scope_modwiki(self): self.r.refresh_access_information(self.refresh_token["modwiki"]) subreddit = self.r.get_subreddit(self.sr) page = subreddit.get_wiki_page("index") page.add_editor(self.other_user_name) page.remove_editor(self.other_user_name) @betamax() def test_scope_modwiki_modcontributors(self): self.r.refresh_access_information(self.refresh_token["modwiki+contr"]) subreddit = self.r.get_subreddit(self.sr) subreddit.add_wiki_ban(self.other_user_name) subreddit.remove_wiki_ban(self.other_user_name) subreddit.add_wiki_contributor(self.other_user_name) subreddit.remove_wiki_contributor(self.other_user_name) @betamax() def test_scope_creddits(self): # Assume there are insufficient creddits. self.r.refresh_access_information(self.refresh_token["creddits"]) redditor = self.r.get_redditor("bboe") sub = self.r.get_submission(url=self.comment_url) # Test error conditions self.assertRaises(TypeError, sub.gild, months=1) for value in (False, 0, -1, "0", "-1"): self.assertRaises(TypeError, redditor.gild, value) # Test object gilding self.assertRaises(errors.InsufficientCreddits, redditor.gild) self.assertRaises(errors.InsufficientCreddits, sub.gild) self.assertRaises(errors.InsufficientCreddits, sub.comments[0].gild) @betamax() def test_scope_privatemessages(self): self.r.refresh_access_information(self.refresh_token["privatemessages"]) self.assertTrue(list(self.r.get_inbox())) @betamax() def test_scope_read(self): self.r.refresh_access_information(self.refresh_token["read"]) self.assertTrue(self.r.get_subreddit(self.priv_sr).subscribers > 0) fullname = "{0}_{1}".format(self.r.config.by_object[Submission], self.priv_submission_id) method1 = self.r.get_info(thing_id=fullname) method2 = self.r.get_submission(submission_id=self.priv_submission_id) self.assertEqual(method1, method2) @betamax() def test_scope_read_get_front_page(self): self.r.refresh_access_information(self.refresh_token["mysubreddits"]) subscribed = list(self.r.get_my_subreddits(limit=None)) self.r.refresh_access_information(self.refresh_token["read"]) for post in self.r.get_front_page(): self.assertTrue(post.subreddit in subscribed) @betamax() def test_scope_read_get_sub_listingr(self): self.r.refresh_access_information(self.refresh_token["read"]) subreddit = self.r.get_subreddit(self.priv_sr) self.assertTrue(list(subreddit.get_top())) @betamax() def test_scope_read_get_submission_by_url(self): url = "https://www.reddit.com/r/reddit_api_test_priv/comments/16kbb7/" "google/" self.r.refresh_access_information(self.refresh_token["read"]) submission = Submission.from_url(self.r, url) self.assertTrue(submission.num_comments != 0) @betamax() def test_scope_read_priv_sr_comments(self): self.r.refresh_access_information(self.refresh_token["read"]) self.assertTrue(list(self.r.get_comments(self.priv_sr))) @betamax() def test_scope_wikiread_wiki_page(self): self.r.refresh_access_information(self.refresh_token["wikiread"]) self.assertTrue(self.r.get_wiki_page(self.sr, "index")) @betamax() def test_scope_read_priv_sub_comments(self): self.r.refresh_access_information(self.refresh_token["read"]) submission = Submission.from_id(self.r, self.priv_submission_id) self.assertTrue(submission.comments) @betamax() def test_scope_submit(self): self.r.refresh_access_information(self.refresh_token["submit"]) result = self.r.submit(self.sr, "OAuth Submit", text="Foo") self.assertTrue(isinstance(result, Submission)) @betamax() def test_scope_subscribe(self): self.r.refresh_access_information(self.refresh_token["subscribe"]) self.r.get_subreddit(self.sr).subscribe() @betamax() def test_scope_vote(self): self.r.refresh_access_information(self.refresh_token["vote"]) submission = Submission.from_id(self.r, self.submission_edit_id) submission.clear_vote() @betamax() def test_set_access_credentials(self): self.assertTrue(self.r.user is None) result = self.r.refresh_access_information(self.refresh_token["identity"], update_session=False) self.assertTrue(self.r.user is None) self.r.set_access_credentials(**result) self.assertFalse(self.r.user is None) @betamax() def test_oauth_without_identy_doesnt_set_user(self): self.assertTrue(self.r.user is None) self.r.refresh_access_information(self.refresh_token["edit"]) self.assertTrue(self.r.user is None)
class SubRedditStats(object): post_prefix = tt('Subreddit Stats:') post_header = tt('---\n###{0}\n') post_footer = tt('>Generated with [BBoe](/u/bboe)\'s [Subreddit Stats]' '(https://github.com/praw-dev/prawtools) \n{0}' 'SRS Marker: {1}') re_marker = re.compile('SRS Marker: (\d+)') @staticmethod def _previous_max(submission): try: val = SubRedditStats.re_marker.findall(submission.selftext)[-1] return float(val) except (IndexError, TypeError): print('End marker not found in previous submission. Aborting') sys.exit(1) @staticmethod def _permalink(permalink): tokens = permalink.split('/') if tokens[8] == '': # submission return tt('/comments/{0}/_/').format(tokens[6]) else: # comment return tt('/comments/{0}/_/{1}?context=1').format(tokens[6], tokens[8]) @staticmethod def _user(user): if user is None: return '_deleted_' elif isinstance(user, Redditor): user = str(user) return tt('[{0}](/user/{1})').format(user.replace('_', '\_'), user) @staticmethod def _submit(func, *args, **kwargs): def sleep(sleep_time): print('\tSleeping for {0} seconds'.format(sleep_time)) time.sleep(sleep_time) while True: try: return func(*args, **kwargs) except RateLimitExceeded as error: sleep(error.sleep_time) except ExceptionList as exception_list: for error in exception_list.errors: if isinstance(error, RateLimitExceeded): sleep(error.sleep_time) break else: raise def __init__(self, subreddit, site, verbosity): self.reddit = Reddit(str(self), site) self.subreddit = self.reddit.get_subreddit(subreddit) self.verbosity = verbosity self.submissions = [] self.comments = [] self.submitters = defaultdict(list) self.commenters = defaultdict(list) self.min_date = 0 self.max_date = time.time() - DAYS_IN_SECONDS * 3 self.prev_srs = None # Config self.reddit.config.comment_limit = -1 # Fetch max comments possible self.reddit.config.comment_sort = 'top' def login(self, user, pswd): if self.verbosity > 0: print('Logging in') self.reddit.login(user, pswd) def msg(self, msg, level, overwrite=False): if self.verbosity >= level: sys.stdout.write(msg) if overwrite: sys.stdout.write('\r') sys.stdout.flush() else: sys.stdout.write('\n') def prev_stat(self, prev_url): submission = self.reddit.get_submission(prev_url) self.min_date = self._previous_max(submission) self.prev_srs = prev_url def fetch_recent_submissions(self, max_duration, after, exclude_self, since_last=True): '''Fetches recent submissions in subreddit with boundaries. Does not include posts within the last three days as their scores may not be representative. Keyword arguments: max_duration -- When set, specifies the number of days to include after -- When set, fetch all submission after this submission id. exclude_self -- When true, don't include self posts. since_last -- When true use info from last submission to determine the stop point ''' if max_duration: self.min_date = self.max_date - DAYS_IN_SECONDS * max_duration params = {'after': after} if after else None self.msg('DEBUG: Fetching submissions', 1) for submission in self.subreddit.get_new_by_date(limit=None, params=params): if submission.created_utc > self.max_date: continue if submission.created_utc <= self.min_date: break if (since_last and str(submission.author) == str(self.reddit.user) and submission.title.startswith(self.post_prefix)): # Use info in this post to update the min_date # And don't include this post self.msg(tt('Found previous: {0}') .format(safe_title(submission)), 2) if self.prev_srs is None: # Only use the most recent self.min_date = max(self.min_date, self._previous_max(submission)) self.prev_srs = submission.permalink continue if exclude_self and submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def fetch_top_submissions(self, top, exclude_self): '''Fetches top 1000 submissions by some top value. Keyword arguments: top -- One of week, month, year, all exclude_self -- When true, don't include self posts. ''' if top not in ('day', 'week', 'month', 'year', 'all'): raise TypeError('{0!r} is not a valid top value'.format(top)) self.msg('DEBUG: Fetching submissions', 1) params = {'t': top} for submission in self.subreddit.get_top(limit=None, params=params): if exclude_self and submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def process_submitters(self): self.msg('DEBUG: Processing Submitters', 1) for submission in self.submissions: if submission.author: self.submitters[str(submission.author)].append(submission) def process_commenters(self): num = len(self.submissions) self.msg('DEBUG: Processing Commenters on {0} submissions'.format(num), 1) for i, submission in enumerate(self.submissions): self.msg('{0}/{1} submissions'.format(i + 1, num), 2, overwrite=True) if submission.num_comments == 0: continue try: self.comments.extend(submission.all_comments_flat) except Exception as exception: print('Exception fetching comments on {0!r}: {1}'.format( submission.content_id, str(exception))) for orphans in itervalues(submission._orphaned): self.comments.extend(orphans) for comment in self.comments: if comment.author: self.commenters[str(comment.author)].append(comment) def basic_stats(self): sub_ups = sum(x.ups for x in self.submissions) sub_downs = sum(x.downs for x in self.submissions) comm_ups = sum(x.ups for x in self.comments) comm_downs = sum(x.downs for x in self.comments) if sub_ups > 0 or sub_downs > 0: sub_up_perc = sub_ups * 100 / (sub_ups + sub_downs) else: sub_up_perc = 100 if comm_ups > 0 or comm_downs > 0: comm_up_perc = comm_ups * 100 / (comm_ups + comm_downs) else: comm_up_perc = 100 values = [('Total', len(self.submissions), '', len(self.comments), ''), ('Unique Redditors', len(self.submitters), '', len(self.commenters), ''), ('Upvotes', sub_ups, '{0}%'.format(sub_up_perc), comm_ups, '{0}%'.format(comm_up_perc)), ('Downvotes', sub_downs, '{0}%'.format(100 - sub_up_perc), comm_downs, '{0}%'.format(100 - comm_up_perc))] retval = '||Submissions|%|Comments|%|\n:-:|--:|--:|--:|--:\n' for quad in values: retval += '__{0}__|{1}|{2}|{3}|{4}\n'.format(*quad) return retval + '\n' def top_submitters(self, num, num_submissions): num = min(num, len(self.submitters)) if num <= 0: return '' top_submitters = sorted(iteritems(self.submitters), reverse=True, key=lambda x: (sum(y.score for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Submitters\' Top Submissions') for (author, submissions) in top_submitters: retval += '0. {0} pts, {1} submissions: {2}\n'.format( sum(x.score for x in submissions), len(submissions), self._user(author)) for sub in sorted(submissions, reverse=True, key=lambda x: x.score)[:num_submissions]: title = safe_title(sub) if sub.permalink != sub.url: retval += tt(' 0. [{0}]({1})').format(title, sub.url) else: retval += tt(' 0. {0}').format(title) retval += ' ({0} pts, [{1} comments]({2}))\n'.format( sub.score, sub.num_comments, self._permalink(sub.permalink)) retval += '\n' return retval def top_commenters(self, num): score = lambda x: x.ups - x.downs num = min(num, len(self.commenters)) if num <= 0: return '' top_commenters = sorted(iteritems(self.commenters), reverse=True, key=lambda x: (sum(score(y) for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Commenters') for author, comments in top_commenters: retval += '0. {0} ({1} pts, {2} comments)\n'.format( self._user(author), sum(score(x) for x in comments), len(comments)) return '{0}\n'.format(retval) def top_submissions(self, num): num = min(num, len(self.submissions)) if num <= 0: return '' top_submissions = sorted(self.submissions, reverse=True, key=lambda x: x.score)[:num] retval = self.post_header.format('Top Submissions') for sub in top_submissions: title = safe_title(sub) if sub.permalink != sub.url: retval += tt('0. [{0}]({1})').format(title, sub.url) else: retval += tt('0. {0}').format(title) retval += ' by {0} ({1} pts, [{2} comments]({3}))\n'.format( self._user(sub.author), sub.score, sub.num_comments, self._permalink(sub.permalink)) return tt('{0}\n').format(retval) def top_comments(self, num): score = lambda x: x.ups - x.downs num = min(num, len(self.comments)) if num <= 0: return '' top_comments = sorted(self.comments, reverse=True, key=score)[:num] retval = self.post_header.format('Top Comments') for comment in top_comments: title = safe_title(comment.submission) retval += tt('0. {0} pts: {1}\'s [comment]({2}) in {3}\n').format( score(comment), self._user(comment.author), self._permalink(comment.permalink), title) return tt('{0}\n').format(retval) def publish_results(self, subreddit, submitters, commenters, submissions, comments, top, debug=False): def timef(timestamp, date_only=False): dtime = datetime.fromtimestamp(timestamp) if date_only: retval = dtime.strftime('%Y-%m-%d') else: retval = dtime.strftime('%Y-%m-%d %H:%M PDT') return retval if self.prev_srs: prev = '[Prev SRS]({0}) \n'.format(self._permalink(self.prev_srs)) else: prev = '' basic = self.basic_stats() t_commenters = self.top_commenters(commenters) t_submissions = self.top_submissions(submissions) t_comments = self.top_comments(comments) footer = self.post_footer.format(prev, self.max_date) body = '' num_submissions = 10 while body == '' or len(body) > MAX_BODY_SIZE and num_submissions > 2: t_submitters = self.top_submitters(submitters, num_submissions) body = (basic + t_submitters + t_commenters + t_submissions + t_comments + footer) num_submissions -= 1 if len(body) > MAX_BODY_SIZE: print('The resulting message is too big. Not submitting.') debug = True # Set the initial title base_title = '{0} {1} {2}posts from {3} to {4}'.format( self.post_prefix, str(self.subreddit), 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) submitted = False while not debug and not submitted: if subreddit: # Verify the user wants to submit to the subreddit msg = ('You are about to submit to subreddit {0!r} as {1!r}.\n' 'Are you sure? yes/[no]: '.format( subreddit, str(self.reddit.user))) sys.stdout.write(msg) sys.stdout.flush() if sys.stdin.readline().strip().lower() not in ['y', 'yes']: subreddit = None elif not subreddit: # Prompt for the subreddit to submit to msg = ('Please enter a subreddit to submit to (press return to' ' abort): ') sys.stdout.write(msg) sys.stdout.flush() subreddit = sys.stdin.readline().strip() if not subreddit: print('Submission aborted\n') debug = True # Vary the title depending on where posting if str(self.subreddit) == subreddit: title = '{0} {1}posts from {2} to {3}'.format( self.post_prefix, 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) else: title = base_title if subreddit: # Attempt to make the submission try: res = self._submit(self.reddit.submit, subreddit, title, text=body) print(res.permalink) submitted = True except Exception as error: raise print('The submission failed:' + str(error)) subreddit = None if not submitted: print(base_title) print(body)
class SubRedditStats(object): """Contain all the functionality of the subreddit_stats command.""" post_prefix = tt('Subreddit Stats:') post_header = tt('---\n###{0}\n') post_footer = tt('>Generated with [BBoe](/u/bboe)\'s [Subreddit Stats]' '(https://github.com/praw-dev/prawtools) \n{0}' 'SRS Marker: {1}') re_marker = re.compile(r'SRS Marker: (\d+)') @staticmethod def _previous_max(submission): try: val = SubRedditStats.re_marker.findall(submission.selftext)[-1] return float(val) except (IndexError, TypeError): print('End marker not found in previous submission. Aborting') sys.exit(1) @staticmethod def _permalink(permalink): tokens = permalink.split('/') if tokens[8] == '': # submission return tt('/comments/{0}/_/').format(tokens[6]) else: # comment return tt('/comments/{0}/_/{1}?context=1').format( tokens[6], tokens[8]) @staticmethod def _pts(points): return '1 pt' if points == 1 else '{0} pts'.format(points) @staticmethod def _user(user): if user is None: return '_deleted_' elif isinstance(user, Redditor): user = str(user) return tt('[{0}](/user/{1})').format(user.replace('_', r'\_'), user) @staticmethod def _submit(func, *args, **kwargs): def sleep(sleep_time): print('\tSleeping for {0} seconds'.format(sleep_time)) time.sleep(sleep_time) while True: try: return func(*args, **kwargs) except RateLimitExceeded as error: sleep(error.sleep_time) except ExceptionList as exception_list: for error in exception_list.errors: if isinstance(error, RateLimitExceeded): sleep(error.sleep_time) break else: raise def __init__(self, subreddit, site, verbosity, distinguished): """Initialize the SubRedditStats instance with config options.""" self.reddit = Reddit(str(self), site, disable_update_check=True) self.subreddit = self.reddit.get_subreddit(subreddit) self.verbosity = verbosity self.distinguished = distinguished self.submissions = [] self.comments = [] self.submitters = defaultdict(list) self.commenters = defaultdict(list) self.min_date = 0 self.max_date = time.time() - DAYS_IN_SECONDS * 3 self.prev_srs = None def login(self, user, pswd): """Login and provide debugging output if so wanted.""" if self.verbosity > 0: print('Logging in') self.reddit.login(user, pswd) def msg(self, msg, level, overwrite=False): """Output a messaage to the screen if the verbosity is sufficient.""" if self.verbosity and self.verbosity >= level: sys.stdout.write(msg) if overwrite: sys.stdout.write('\r') sys.stdout.flush() else: sys.stdout.write('\n') def prev_stat(self, prev_url): """Load the previous subreddit stats page.""" submission = self.reddit.get_submission(prev_url) self.min_date = self._previous_max(submission) self.prev_srs = prev_url def fetch_recent_submissions(self, max_duration, after, exclude_self, exclude_link, since_last=True): """Fetch recent submissions in subreddit with boundaries. Does not include posts within the last three days as their scores may not be representative. :param max_duration: When set, specifies the number of days to include :param after: When set, fetch all submission after this submission id. :param exclude_self: When true, don't include self posts. :param exclude_link: When true, don't include links. :param since_last: When true use info from last submission to determine the stop point :returns: True if any submissions were found. """ if exclude_self and exclude_link: raise TypeError('Cannot set both exclude_self and exclude_link.') if max_duration: self.min_date = self.max_date - DAYS_IN_SECONDS * max_duration params = {'after': after} if after else None self.msg('DEBUG: Fetching submissions', 1) for submission in self.subreddit.get_new(limit=None, params=params): if submission.created_utc > self.max_date: continue if submission.created_utc <= self.min_date: break if since_last and str(submission.author) == str(self.reddit.user) \ and submission.title.startswith(self.post_prefix): # Use info in this post to update the min_date # And don't include this post self.msg( tt('Found previous: {0}').format(safe_title(submission)), 2) if self.prev_srs is None: # Only use the most recent self.min_date = max(self.min_date, self._previous_max(submission)) self.prev_srs = submission.permalink continue if exclude_self and submission.is_self: continue if exclude_link and not submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def fetch_top_submissions(self, top, exclude_self, exclude_link): """Fetch top 1000 submissions by some top value. :param top: One of week, month, year, all :param exclude_self: When true, don't include self posts. :param exclude_link: When true, include only self posts :returns: True if any submissions were found. """ if exclude_self and exclude_link: raise TypeError('Cannot set both exclude_self and exclude_link.') if top not in ('day', 'week', 'month', 'year', 'all'): raise TypeError('{0!r} is not a valid top value'.format(top)) self.msg('DEBUG: Fetching submissions', 1) params = {'t': top} for submission in self.subreddit.get_top(limit=None, params=params): if exclude_self and submission.is_self: continue if exclude_link and not submission.is_self: continue self.submissions.append(submission) num_submissions = len(self.submissions) self.msg('DEBUG: Found {0} submissions'.format(num_submissions), 1) if num_submissions == 0: return False # Update real min and max dates self.submissions.sort(key=lambda x: x.created_utc) self.min_date = self.submissions[0].created_utc self.max_date = self.submissions[-1].created_utc return True def process_submitters(self): """Group submissions by author.""" self.msg('DEBUG: Processing Submitters', 1) for submission in self.submissions: if submission.author and (self.distinguished or submission.distinguished is None): self.submitters[str(submission.author)].append(submission) def process_commenters(self): """Group comments by author.""" num = len(self.submissions) self.msg('DEBUG: Processing Commenters on {0} submissions'.format(num), 1) for i, submission in enumerate(self.submissions): # Explicitly fetch as many comments as possible by top sort # Note that this is the first time the complete submission object # is obtained. Only a partial object was returned when getting the # subreddit listings. try: submission = self.reddit.get_submission(submission.permalink, comment_limit=None, comment_sort='top') except HTTPError as exc: print('Ignoring comments on {0} due to HTTP status {1}'.format( submission.url, exc.response.status_code)) continue self.msg('{0}/{1} submissions'.format(i + 1, num), 2, overwrite=True) if submission.num_comments == 0: continue skipped = submission.replace_more_comments() if skipped: skip_num = sum(x.count for x in skipped) print('Ignored {0} comments ({1} MoreComment objects)'.format( skip_num, len(skipped))) comments = [ x for x in flatten_tree(submission.comments) if self.distinguished or x.distinguished is None ] self.comments.extend(comments) # pylint: disable=W0212 for orphans in itervalues(submission._orphaned): self.comments.extend(orphans) # pylint: enable=W0212 for comment in self.comments: if comment.author: self.commenters[str(comment.author)].append(comment) def basic_stats(self): """Return a markdown representation of simple statistics.""" sub_score = sum(x.score for x in self.submissions) comm_score = sum(x.score for x in self.comments) sub_duration = self.max_date - self.min_date sub_rate = (86400. * len(self.submissions) / sub_duration if sub_duration else len(self.submissions)) # Compute comment rate if self.comments: self.comments.sort(key=lambda x: x.created_utc) duration = (self.comments[-1].created_utc - self.comments[0].created_utc) comm_rate = (86400. * len(self.comments) / duration if duration else len(self.comments)) else: comm_rate = 0 values = [('Total', len(self.submissions), len(self.comments)), ('Rate (per day)', '{0:.2f}'.format(sub_rate), '{0:.2f}'.format(comm_rate)), ('Unique Redditors', len(self.submitters), len(self.commenters)), ('Combined Score', sub_score, comm_score)] retval = 'Period: {0:.2f} days\n\n'.format(sub_duration / 86400.) retval += '||Submissions|Comments|\n:-:|--:|--:\n' for quad in values: # pylint: disable=W0142 retval += '__{0}__|{1}|{2}\n'.format(*quad) # pylint: enable=W0142 return retval + '\n' def top_submitters(self, num, num_submissions): """Return a markdown representation of the top submitters.""" num = min(num, len(self.submitters)) if num <= 0: return '' top_submitters = sorted(iteritems(self.submitters), reverse=True, key=lambda x: (sum(y.score for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Submitters\' Top Submissions') for (author, submissions) in top_submitters: retval += '0. {0}, {1} submission{2}: {3}\n'.format( self._pts(sum(x.score for x in submissions)), len(submissions), 's' if len(submissions) > 1 else '', self._user(author)) for sub in sorted(submissions, reverse=True, key=lambda x: x.score)[:num_submissions]: title = safe_title(sub) if sub.permalink != sub.url: retval += tt(' 0. [{0}]({1})').format(title, sub.url) else: retval += tt(' 0. {0}').format(title) retval += ' ({0}, [{1} comment{2}]({3}))\n'.format( self._pts(sub.score), sub.num_comments, 's' if sub.num_comments > 1 else '', self._permalink(sub.permalink)) retval += '\n' return retval def top_commenters(self, num): """Return a markdown representation of the top commenters.""" score = lambda x: x.score num = min(num, len(self.commenters)) if num <= 0: return '' top_commenters = sorted(iteritems(self.commenters), reverse=True, key=lambda x: (sum(score(y) for y in x[1]), len(x[1])))[:num] retval = self.post_header.format('Top Commenters') for author, comments in top_commenters: retval += '0. {0} ({1}, {2} comment{3})\n'.format( self._user(author), self._pts(sum(score(x) for x in comments)), len(comments), 's' if len(comments) > 1 else '') return '{0}\n'.format(retval) def top_submissions(self, num): """Return a markdown representation of the top submissions.""" num = min(num, len(self.submissions)) if num <= 0: return '' top_submissions = sorted([ x for x in self.submissions if self.distinguished or x.distinguished is None ], reverse=True, key=lambda x: x.score)[:num] if not top_submissions: return '' retval = self.post_header.format('Top Submissions') for sub in top_submissions: title = safe_title(sub) if sub.permalink != sub.url: retval += tt('0. [{0}]({1})').format(title, sub.url) else: retval += tt('0. {0}').format(title) retval += ' by {0} ({1}, [{2} comment{3}]({4}))\n'.format( self._user(sub.author), self._pts(sub.score), sub.num_comments, 's' if sub.num_comments > 1 else '', self._permalink(sub.permalink)) return tt('{0}\n').format(retval) def top_comments(self, num): """Return a markdown representation of the top comments.""" score = lambda x: x.score num = min(num, len(self.comments)) if num <= 0: return '' top_comments = sorted(self.comments, reverse=True, key=score)[:num] retval = self.post_header.format('Top Comments') for comment in top_comments: title = safe_title(comment.submission) retval += tt('0. {0}: {1}\'s [comment]({2}) in {3}\n').format( self._pts(score(comment)), self._user(comment.author), self._permalink(comment.permalink), title) return tt('{0}\n').format(retval) def publish_results(self, subreddit, submitters, commenters, submissions, comments, top, debug=False): """Submit the results to the subreddit. Has no return value (None).""" def timef(timestamp, date_only=False): """Return a suitable string representaation of the timestamp.""" dtime = datetime.fromtimestamp(timestamp) if date_only: retval = dtime.strftime('%Y-%m-%d') else: retval = dtime.strftime('%Y-%m-%d %H:%M PDT') return retval if self.prev_srs: prev = '[Prev SRS]({0}) \n'.format(self._permalink(self.prev_srs)) else: prev = '' basic = self.basic_stats() t_commenters = self.top_commenters(commenters) t_submissions = self.top_submissions(submissions) t_comments = self.top_comments(comments) footer = self.post_footer.format(prev, self.max_date) body = '' num_submissions = 10 while body == '' or len(body) > MAX_BODY_SIZE and num_submissions > 2: t_submitters = self.top_submitters(submitters, num_submissions) body = (basic + t_submitters + t_commenters + t_submissions + t_comments + footer) num_submissions -= 1 if len(body) > MAX_BODY_SIZE: print('The resulting message is too big. Not submitting.') debug = True # Set the initial title base_title = '{0} {1} {2}posts from {3} to {4}'.format( self.post_prefix, str(self.subreddit), 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) submitted = False while not debug and not submitted: if subreddit: # Verify the user wants to submit to the subreddit msg = ('You are about to submit to subreddit {0!r} as {1!r}.\n' 'Are you sure? yes/[no]: '.format( subreddit, str(self.reddit.user))) sys.stdout.write(msg) sys.stdout.flush() if sys.stdin.readline().strip().lower() not in ['y', 'yes']: subreddit = None elif not subreddit: # Prompt for the subreddit to submit to msg = ('Please enter a subreddit to submit to (press return to' ' abort): ') sys.stdout.write(msg) sys.stdout.flush() subreddit = sys.stdin.readline().strip() if not subreddit: print('Submission aborted\n') debug = True # Vary the title depending on where posting if str(self.subreddit) == subreddit: title = '{0} {1}posts from {2} to {3}'.format( self.post_prefix, 'top ' if top else '', timef(self.min_date, True), timef(self.max_date)) else: title = base_title if subreddit: # Attempt to make the submission try: res = self._submit(self.reddit.submit, subreddit, title, text=body) print(res.permalink) submitted = True except Exception as error: # pylint: disable=W0703 print('The submission failed:' + str(error)) subreddit = None if not submitted: print(base_title) print(body) def save_csv(self, filename): """Create csv file containing comments and submissions by author.""" redditors = set(self.submitters.keys()).union(self.commenters.keys()) mapping = dict((x.lower(), x) for x in redditors) with codecs.open(filename, 'w', encoding='utf-8') as outfile: outfile.write('username, type, permalink, score\n') for _, redditor in sorted(mapping.items()): for submission in self.submitters.get(redditor, []): outfile.write(u'{0}, submission, {1}, {2}\n'.format( redditor, submission.permalink, submission.score)) for comment in self.commenters.get(redditor, []): outfile.write(u'{0}, comment, {1}, {2}\n'.format( redditor, comment.permalink, comment.score))
error("Couldn't login to reddit: {0}".format(e)) # create DB session engine = create_engine(DB_PATH) Base.metadata.bind = engine DBSession = sessionmaker(bind=engine) session = DBSession() if __name__ == '__main__': request_session = requests.session() for post in session.query(Post).all(): # load reddit post submission = reddit.get_submission(submission_id=post.reddit_post_id) # load lepra post debug("Fetching leprosorium post #{0}...".format(post.lepra_post_id)) lepra_url = "http://auto.leprosorium.ru/comments/{0}".format(post.lepra_post_id) response = request_session.get(lepra_url, cookies=LEPROSORIUM_COOKIES) bs_tree = BeautifulSoup(response.text) # parse lepra post debug("Parsing comments...") # for comment in comments for node in bs_tree.findAll('div', attrs={'class': 'post', 'class': 'indent_0'}): comment_id = int(node['id']) debug("Parsing comment #{0}:".format(comment_id)) content = node.find('div', attrs={'class': 'dt'})