def test_tweet(self): consumer_key = settings.TWITTER_CONSUMER_KEY consumer_secret = settings.TWITTER_CONSUMER_SECRET access_key = settings.TWITTER_ACCESS_TOKEN_KEY access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET if any(not i for i in [ consumer_key, consumer_secret, access_key, access_secret, ]): LOG.warning("Twitter API credentials not set!") return api = ThreadedApi( consumer_key=consumer_key, consumer_secret=consumer_secret, access_token_key=access_key, access_token_secret=access_secret, ) message = "This is a test long tweet\r\n{}".format("\r\n".join( f"This is line {line}" for line in range(2, 30))) try: if calc_expected_status_length(message) > CHARACTER_LIMIT: api.PostUpdates(message, continuation="…", threaded=True) else: api.PostUpdate(message) except TwitterError as exc: LOG.warning("Hit twitter error: %s", exc) if str(exc) in RETRYABLE_ERRORS: raise self.retry(exc=exc) delay = get_twitter_rate_limit_delay(api) if delay is None: LOG.error("No idea what to do with twitter error %s", exc) raise raise self.retry(countdown=delay, exc=exc)
def tweet_about_beers(self, beer_pks): if not beer_pks: LOG.warning("nothing to do") return LOG.debug("Tweeting about beer PKs: %s", beer_pks) consumer_key = settings.TWITTER_CONSUMER_KEY consumer_secret = settings.TWITTER_CONSUMER_SECRET access_key = settings.TWITTER_ACCESS_TOKEN_KEY access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET if any(not i for i in [ consumer_key, consumer_secret, access_key, access_secret, ]): LOG.warning("Twitter API credentials not set!") return api = ThreadedApi( consumer_key=consumer_key, consumer_secret=consumer_secret, access_token_key=access_key, access_token_secret=access_secret, ) # Mark beers which have been removed from the tap list as tweeted about Beer.objects.filter( tweeted_about=False, taps__isnull=True, ).update(tweeted_about=True) beers = list( Beer.objects.filter( id__in=beer_pks, tweeted_about=False, ).select_related( "manufacturer", "style", ).prefetch_related( Prefetch( "taps", queryset=Tap.objects.select_related("venue"), ), ).order_by("id")) already_tweeted_about = set(i.id for i in Beer.objects.filter( tweeted_about=True, id__in=beer_pks, )) unknown_pks = set(beer_pks).difference(already_tweeted_about) LOG.debug("Got %s beers", len(beers)) if not beers: if unknown_pks: LOG.warning("No beers found! Trying again shortly") raise self.retry(countdown=300) LOG.info("everything was already tweeted about. No big deal") return if len(beers) > 10: LOG.info("Too many beers to tweet about at once: %s", len(beers)) beers = beers[:10] if len(beers) == 1: beer = beers[0] if beer.tweeted_about: LOG.info("%s has already been tweeted about; skipping.", beer) return message = format_beer(beer, SINGLE_BEER_TEMPLATE).strip() messages = [message] else: extra_beers = len(beer_pks) - len(beers) - len(already_tweeted_about) messages = [ MULTI_BEER_OUTER.format( len(beers), "({} still to come!)".format(extra_beers) if extra_beers > 0 else "", ).strip() ] + format_beers(beers) if len(messages) == 1: LOG.info("All beers already tweeted about") return message = "\r\n".join(messages) LOG.info("Going to tweet: %s", message) try: if calc_expected_status_length(message) > CHARACTER_LIMIT: api.PostUpdates(message, continuation="…", threaded=True) else: api.PostUpdate(message) except TwitterError as exc: LOG.warning("Hit twitter error: %s", exc) if str(exc) in RETRYABLE_ERRORS: raise self.retry(exc=exc) LOG.error("Tweet(s) that caused error was %s", message) delay = get_twitter_rate_limit_delay(api) if delay is None: LOG.error("No idea what to do with twitter error %s", exc) raise raise self.retry(countdown=delay, exc=exc) Beer.objects.filter(id__in=[i.id for i in beers]).update(tweeted_about=True) LOG.debug("Done tweeting")
class TestThreadedApi(TestCase): def setUp(self): self.rng = SystemRandom() self.api = ThreadedApi() self.fake_last_status = None # just silence the config check # pylint: disable=protected-access self.api._config = True def test_single_tweet(self): msg = 'This is a short message' with patch.object(self.api, 'PostUpdate') as mock_post_update: self.api.PostUpdates(msg) mock_post_update.assert_called_once_with(status=msg) def test_long_tweet(self): words = [ ''.join(self.rng.choices(ascii_lowercase, k=25)) for dummy in range(25) ] msg = ' '.join(words) def get_fake_id(status, in_reply_to_status_id=None): if not self.fake_last_status: self.assertIsNone(in_reply_to_status_id) else: self.assertEqual(in_reply_to_status_id, self.fake_last_status.id) self.assertIn(status, msg) next_status = FakeStatus() self.fake_last_status = next_status return next_status with patch.object(self.api, 'PostUpdate') as mock_post_update: mock_post_update.side_effect = get_fake_id self.api.PostUpdates(msg, threaded=True) calls = mock_post_update.call_args_list # 625 chars + 50 spaces ==> 3 tweets self.assertEqual(len(calls), 3) def test_long_tweet_no_thread(self): words = [ ''.join(self.rng.choices(ascii_lowercase, k=25)) for dummy in range(25) ] msg = ' '.join(words) def get_fake_id(status): self.assertIn(status, msg) next_status = FakeStatus() self.fake_last_status = next_status return next_status with patch.object(self.api, 'PostUpdate') as mock_post_update: mock_post_update.side_effect = get_fake_id self.api.PostUpdates(msg, threaded=False) calls = mock_post_update.call_args_list # 625 chars + 50 spaces ==> 3 tweets self.assertEqual(len(calls), 3) def test_break_tweet_up_by_lines(self): tweet = '\r\n'.join([ 'Line 1', 'Line 2', 'Line 3', # line 4 is 301 chars. Yay. 'This is an absurdly long line that will become the basis for what' ' should be preserved as line 4 but who knows what Twitter will ' 'do. This needs even more filler since Twitter decided to double' ' its tweet character limit. I have no idea what else to put in' ' here because everything is awful. Enjoy Arby\'s.', 'Line 5', ]) tweets = self.api.split_tweet_by_lines( tweet, character_limit=CHARACTER_LIMIT - len('…')) self.assertEqual(len(tweets), 3, tweets) self.assertEqual( tweets[0], 'Line 1\r\nLine 2\r\nLine 3', ) self.assertEqual( tweets[1], 'This is an absurdly long line that ' 'will become the basis for what' ' should be preserved as line 4 but who knows what Twitter will ' 'do. This needs even more filler since Twitter decided to double' ' its tweet character limit. I have no idea what else to put in ' 'here because everything') self.assertEqual( tweets[2], 'is awful. Enjoy Arby\'s.\r\nLine 5', )
def tweet_about_beers(self, beer_pks): if not beer_pks: LOG.warning('nothing to do') return LOG.debug('Tweeting about beer PKs: %s', beer_pks) consumer_key = settings.TWITTER_CONSUMER_KEY consumer_secret = settings.TWITTER_CONSUMER_SECRET access_key = settings.TWITTER_ACCESS_TOKEN_KEY access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET if any(not i for i in [ consumer_key, consumer_secret, access_key, access_secret, ]): LOG.warning('Twitter API credentials not set!') return api = ThreadedApi( consumer_key=consumer_key, consumer_secret=consumer_secret, access_token_key=access_key, access_token_secret=access_secret, ) beers = list( Beer.objects.filter( id__in=beer_pks, tweeted_about=False, ).select_related( 'manufacturer', 'style', ).prefetch_related( Prefetch( 'taps', queryset=Tap.objects.select_related('venue'), ), ).order_by('id')) already_tweeted_about = set(i.id for i in Beer.objects.filter( tweeted_about=True, id__in=beer_pks, )) unknown_pks = set(beer_pks).difference(already_tweeted_about) LOG.debug('Got %s beers', len(beers)) if not beers: if unknown_pks: LOG.warning('No beers found! Trying again shortly') raise self.retry(countdown=300) LOG.info('everything was already tweeted about. No big deal') return if len(beers) > 10: LOG.info('Too many beers to tweet about at once: %s', len(beers)) beers = beers[:10] if len(beers) == 1: beer = beers[0] if beer.tweeted_about: LOG.info('%s has already been tweeted about; skipping.', beer) return message = format_beer(beer, SINGLE_BEER_TEMPLATE).strip() messages = [message] else: extra_beers = len(beer_pks) - len(beers) - len(already_tweeted_about) messages = [ MULTI_BEER_OUTER.format( len(beers), '({} still to come!)'.format(extra_beers) if extra_beers > 0 else '').strip() ] + format_beers(beers) if len(messages) == 1: LOG.info('All beers already tweeted about') return message = '\r\n'.join(messages) LOG.info('Going to tweet: %s', message) try: if calc_expected_status_length(message) > CHARACTER_LIMIT: api.PostUpdates(message, continuation='…', threaded=True) else: api.PostUpdate(message) except TwitterError as exc: LOG.warning('Hit twitter error: %s', exc) if str(exc) in RETRYABLE_ERRORS: raise self.retry(exc=exc) delay = get_twitter_rate_limit_delay(api) if delay is None: LOG.error('No idea what to do with twitter error %s', exc) raise raise self.retry(countdown=delay, exc=exc) Beer.objects.filter(id__in=[i.id for i in beers]).update(tweeted_about=True) LOG.debug('Done tweeting')
class TestThreadedApi(TestCase): def setUp(self): self.rng = SystemRandom() self.api = ThreadedApi() self.fake_last_status = None # just silence the config check # pylint: disable=protected-access self.api._config = True def test_single_tweet(self): msg = "This is a short message" with patch.object(self.api, "PostUpdate") as mock_post_update: self.api.PostUpdates(msg) mock_post_update.assert_called_once_with(status=msg) def test_chandlers_ford(self): """Test that the Chandlers Ford beers don't cause breakage""" split_tweets = self.api.split_tweet_by_lines(REAL_BREAKAGE, CHARACTER_LIMIT - 1) self.assertEqual(len(split_tweets), 4) for tweet in split_tweets: self.assertLess(len(tweet), CHARACTER_LIMIT, tweet) def test_long_tweet(self): words = [ "".join(self.rng.choices(ascii_lowercase, k=25)) for dummy in range(25) ] msg = " ".join(words) def get_fake_id(status, in_reply_to_status_id=None): if not self.fake_last_status: self.assertIsNone(in_reply_to_status_id) else: self.assertEqual(in_reply_to_status_id, self.fake_last_status.id) self.assertIn(status, msg) next_status = FakeStatus() self.fake_last_status = next_status return next_status with patch.object(self.api, "PostUpdate") as mock_post_update: mock_post_update.side_effect = get_fake_id self.api.PostUpdates(msg, threaded=True) calls = mock_post_update.call_args_list # 625 chars + 50 spaces ==> 3 tweets self.assertEqual(len(calls), 3) def test_long_tweet_no_thread(self): words = [ "".join(self.rng.choices(ascii_lowercase, k=25)) for dummy in range(25) ] msg = " ".join(words) def get_fake_id(status): self.assertIn(status, msg) next_status = FakeStatus() self.fake_last_status = next_status return next_status with patch.object(self.api, "PostUpdate") as mock_post_update: mock_post_update.side_effect = get_fake_id self.api.PostUpdates(msg, threaded=False) calls = mock_post_update.call_args_list # 625 chars + 50 spaces ==> 3 tweets self.assertEqual(len(calls), 3) def test_break_tweet_up_by_lines(self): tweet = "\r\n".join([ "Line 1", "Line 2", "Line 3", # line 4 is 301 chars. Yay. "This is an absurdly long line that will become the basis for what" " should be preserved as line 4 but who knows what Twitter will " "do. This needs even more filler since Twitter decided to double" " its tweet character limit. I have no idea what else to put in" " here because everything is awful. Enjoy Arby's.", "Line 5", ]) tweets = self.api.split_tweet_by_lines( tweet, character_limit=CHARACTER_LIMIT - len("…")) self.assertEqual(len(tweets), 3, tweets) self.assertEqual( tweets[0], "Line 1\r\nLine 2\r\nLine 3", ) self.assertEqual( tweets[1], "This is an absurdly long line that " "will become the basis for what" " should be preserved as line 4 but who knows what Twitter will " "do. This needs even more filler since Twitter decided to double" " its tweet character limit. I have no idea what else to put in " "here because everything", ) self.assertEqual( tweets[2], "is awful. Enjoy Arby's.\r\nLine 5", )