Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-tweepy for openSUSE:Factory checked in at 2026-08-22 21:36:30 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-tweepy (Old) and /work/SRC/openSUSE:Factory/.python-tweepy.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-tweepy" Sat Aug 22 21:36:30 2026 rev:20 rq:1373077 version:4.17.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-tweepy/python-tweepy.changes 2026-01-30 18:23:04.575516979 +0100 +++ /work/SRC/openSUSE:Factory/.python-tweepy.new.1258/python-tweepy.changes 2026-08-22 21:38:38.310574529 +0200 @@ -1,0 +2,7 @@ +Sat Aug 22 13:53:49 UTC 2026 - Dirk Müller <[email protected]> + +- update to 4.17.0: + * Add reset_time parameter to TooManyRequests + * Replace deprecated datetime.utcnow() in MongodbCache.store + +------------------------------------------------------------------- Old: ---- v4.16.0.tar.gz New: ---- v4.17.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-tweepy.spec ++++++ --- /var/tmp/diff_new_pack.i7DWpS/_old 2026-08-22 21:38:38.986598742 +0200 +++ /var/tmp/diff_new_pack.i7DWpS/_new 2026-08-22 21:38:38.988598814 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-tweepy -Version: 4.16.0 +Version: 4.17.0 Release: 0 Summary: Twitter library for python License: MIT ++++++ v4.16.0.tar.gz -> v4.17.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/.github/workflows/publish.yml new/tweepy-4.17.0/.github/workflows/publish.yml --- old/tweepy-4.16.0/.github/workflows/publish.yml 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/.github/workflows/publish.yml 2026-07-02 21:08:52.000000000 +0200 @@ -1,9 +1,8 @@ name: Build and Publish Tweepy to PyPI on: - push: - tags: - - "v*.*.*" + release: + types: [published] jobs: build: @@ -55,8 +54,8 @@ - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - github-release: - name: Sign distribution and create the Github release + upload-to-release: + name: Sign distribution and upload to Github release needs: - publish-to-pypi runs-on: ubuntu-latest @@ -77,14 +76,6 @@ inputs: >- ./dist/*.tar.gz ./dist/*.whl - - name: Create Github release - env: - GITHUB_TOKEN: ${{ github.token }} - run: >- - gh release create - "$GITHUB_REF_NAME" - --repo "$GITHUB_REPOSITORY" - --notes "" - name: Upload to Github release env: GITHUB_TOKEN: ${{ github.token }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/docs/changelog.md new/tweepy-4.17.0/docs/changelog.md --- old/tweepy-4.16.0/docs/changelog.md 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/docs/changelog.md 2026-07-02 21:08:52.000000000 +0200 @@ -975,7 +975,7 @@ - https://dev.twitter.com/docs/streaming-apis/messages#Disconnect_messages_disconnect - [Compare View](https://github.com/tweepy/tweepy/compare/2.1...2.2) - Use HTTPS by default. - - Support setting the starting cursor postion (ex: Ex: + - Support setting the starting cursor position (ex: Ex: Cursor(api.friends_ids, cursor=123456)) - Added API.cached_result instance flag that is "True" when cached result is returned. - New Streaming client callbacks diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/docs/streaming.rst new/tweepy-4.17.0/docs/streaming.rst --- old/tweepy-4.16.0/docs/streaming.rst 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/docs/streaming.rst 2026-07-02 21:08:52.000000000 +0200 @@ -36,7 +36,7 @@ .. _Building rules for filtered stream: https://developer.twitter.com/en/docs/twitter-api/tweets/filtered-stream/integrate/build-a-rule Data received from the stream is passed to :meth:`StreamingClient.on_data`. -This method handles sending the data to other methods. Tweets recieved are sent +This method handles sending the data to other methods. Tweets received are sent to :meth:`StreamingClient.on_tweet`, ``includes`` data are sent to :meth:`StreamingClient.on_includes`, errors are sent to :meth:`StreamingClient.on_errors`, and matching rules are sent to diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tests/test_exceptions.py new/tweepy-4.17.0/tests/test_exceptions.py --- old/tweepy-4.16.0/tests/test_exceptions.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tweepy-4.17.0/tests/test_exceptions.py 2026-07-02 21:08:52.000000000 +0200 @@ -0,0 +1,105 @@ +import time +import unittest +from unittest.mock import Mock + +from tweepy.errors import TooManyRequests, HTTPException + + +class TooManyRequestsTests(unittest.TestCase): + """Test cases for TooManyRequests exception with reset_time feature""" + + def setUp(self): + """Set up mock response for testing""" + self.mock_response = Mock() + self.mock_response.status_code = 429 + self.mock_response.reason = "Too Many Requests" + self.mock_response.json.return_value = { + "errors": [{"message": "Rate limit exceeded"}] + } + + self.test_reset_time = int(time.time()) + 900 # 15 minutes from now + + def test_too_many_requests_with_reset_time(self): + """Test that TooManyRequests exception stores reset_time correctly""" + exception = TooManyRequests(self.mock_response, reset_time=self.test_reset_time) + + self.assertEqual(exception.reset_time, self.test_reset_time) + self.assertIsInstance(exception, HTTPException) + self.assertEqual(str(exception), "429 Too Many Requests\nRate limit exceeded") + + def test_too_many_requests_without_reset_time(self): + """Test that TooManyRequests exception handles None reset_time""" + exception = TooManyRequests(self.mock_response) + + self.assertIsNone(exception.reset_time) + self.assertIsInstance(exception, HTTPException) + + def test_too_many_requests_explicit_none_reset_time(self): + """Test that TooManyRequests exception handles explicit None reset_time""" + exception = TooManyRequests(self.mock_response, reset_time=None) + + self.assertIsNone(exception.reset_time) + self.assertIsInstance(exception, HTTPException) + + def test_too_many_requests_with_response_json(self): + """Test that TooManyRequests exception works with response_json parameter""" + response_json = {"errors": [{"message": "Custom rate limit message"}]} + exception = TooManyRequests( + self.mock_response, + response_json=response_json, + reset_time=self.test_reset_time + ) + + self.assertEqual(exception.reset_time, self.test_reset_time) + self.assertEqual(str(exception), "429 Too Many Requests\nCustom rate limit message") + + def test_too_many_requests_backward_compatibility(self): + """Test that old constructor usage still works (backward compatibility)""" + # This is how TooManyRequests was called before the enhancement + exception = TooManyRequests(self.mock_response) + + self.assertIsNone(exception.reset_time) + self.assertIsInstance(exception, HTTPException) + # Should still have all the original HTTPException functionality + self.assertEqual(exception.response, self.mock_response) + + def test_too_many_requests_inheritance(self): + """Test that TooManyRequests still properly inherits from HTTPException""" + exception = TooManyRequests(self.mock_response, reset_time=self.test_reset_time) + + # Should inherit all HTTPException attributes + self.assertTrue(hasattr(exception, 'response')) + self.assertTrue(hasattr(exception, 'api_errors')) + self.assertTrue(hasattr(exception, 'api_codes')) + self.assertTrue(hasattr(exception, 'api_messages')) + + # Should have the new reset_time attribute + self.assertTrue(hasattr(exception, 'reset_time')) + self.assertEqual(exception.reset_time, self.test_reset_time) + + def test_too_many_requests_with_zero_reset_time(self): + """Test that TooManyRequests exception handles zero reset_time""" + exception = TooManyRequests(self.mock_response, reset_time=0) + + self.assertEqual(exception.reset_time, 0) + + def test_too_many_requests_with_negative_reset_time(self): + """Test that TooManyRequests exception handles negative reset_time (past time)""" + past_time = int(time.time()) - 3600 # 1 hour ago + exception = TooManyRequests(self.mock_response, reset_time=past_time) + + self.assertEqual(exception.reset_time, past_time) + + def test_too_many_requests_reset_time_type(self): + """Test that reset_time can be different integer types""" + # Test with string that can be converted to int (as it comes from headers) + exception1 = TooManyRequests(self.mock_response, reset_time=str(self.test_reset_time)) + self.assertEqual(exception1.reset_time, str(self.test_reset_time)) + + # Test with actual int + exception2 = TooManyRequests(self.mock_response, reset_time=self.test_reset_time) + self.assertEqual(exception2.reset_time, self.test_reset_time) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tests/test_rate_limit_reset_time.py new/tweepy-4.17.0/tests/test_rate_limit_reset_time.py --- old/tweepy-4.16.0/tests/test_rate_limit_reset_time.py 1970-01-01 01:00:00.000000000 +0100 +++ new/tweepy-4.17.0/tests/test_rate_limit_reset_time.py 2026-07-02 21:08:52.000000000 +0200 @@ -0,0 +1,312 @@ +import asyncio +import time +import unittest +from unittest.mock import Mock, patch, AsyncMock + +from tweepy.client import Client +from tweepy.asynchronous.client import AsyncClient +from tweepy.api import API +from tweepy.errors import TooManyRequests + + +class RateLimitResetTimeTests(unittest.TestCase): + """Test cases for rate limit reset time functionality across all clients""" + + def setUp(self): + """Set up test clients and mock data""" + self.bearer_token = "fake_bearer_token" + self.consumer_key = "fake_consumer_key" + self.consumer_secret = "fake_consumer_secret" + self.access_token = "fake_access_token" + self.access_token_secret = "fake_access_token_secret" + + self.current_time = int(time.time()) + self.reset_time = self.current_time + 900 # 15 minutes from now + self.reset_time_str = str(self.reset_time) + + def create_mock_429_response(self, include_reset_header=True, sync=True): + """Helper to create mock 429 response""" + mock_response = Mock() + if sync: + mock_response.status_code = 429 + else: + mock_response.status = 429 + mock_response.reason = "Too Many Requests" + + headers = {} + if include_reset_header: + headers["x-rate-limit-reset"] = self.reset_time_str + mock_response.headers = headers + + if not sync: + # For async client + mock_response.json = AsyncMock(return_value={ + "errors": [{"message": "Rate limit exceeded"}] + }) + else: + mock_response.json.return_value = { + "errors": [{"message": "Rate limit exceeded"}] + } + + return mock_response + + # Sync Client Tests + def test_sync_client_rate_limit_with_reset_header_wait_false(self): + """Test sync client raises TooManyRequests with reset_time when wait_on_rate_limit=False""" + client = Client(bearer_token=self.bearer_token, wait_on_rate_limit=False) + mock_response = self.create_mock_429_response(include_reset_header=True) + + # Mock the context manager behavior + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=None) + + with patch.object(client.session, 'request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + client.request("GET", "/2/tweets/search/recent") + + exception = cm.exception + self.assertEqual(exception.reset_time, self.reset_time) + + def test_sync_client_rate_limit_without_reset_header_wait_false(self): + """Test sync client raises TooManyRequests with None reset_time when header missing""" + client = Client(bearer_token=self.bearer_token, wait_on_rate_limit=False) + mock_response = self.create_mock_429_response(include_reset_header=False) + + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=None) + + with patch.object(client.session, 'request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + client.request("GET", "/2/tweets/search/recent") + + exception = cm.exception + self.assertIsNone(exception.reset_time) + + @patch('time.sleep') + @patch('time.time') + def test_sync_client_rate_limit_with_reset_header_wait_true(self, mock_time, mock_sleep): + """Test sync client waits correctly when wait_on_rate_limit=True""" + mock_time.return_value = self.current_time + + client = Client(bearer_token=self.bearer_token, wait_on_rate_limit=True) + mock_response_429 = self.create_mock_429_response(include_reset_header=True) + mock_response_200 = Mock() + mock_response_200.status_code = 200 + + # Mock context managers + mock_response_429.__enter__ = Mock(return_value=mock_response_429) + mock_response_429.__exit__ = Mock(return_value=None) + mock_response_200.__enter__ = Mock(return_value=mock_response_200) + mock_response_200.__exit__ = Mock(return_value=None) + + # First call returns 429, second call returns 200 + with patch.object(client.session, 'request', side_effect=[mock_response_429, mock_response_200]): + result = client.request("GET", "/2/tweets/search/recent") + + # Should have slept for the calculated time + expected_sleep_time = self.reset_time - self.current_time + 1 + mock_sleep.assert_called_once_with(expected_sleep_time) + self.assertEqual(result, mock_response_200) + + @patch('time.sleep') + @patch('time.time') + def test_sync_client_rate_limit_without_reset_header_wait_true(self, mock_time, mock_sleep): + """Test sync client handles missing header when wait_on_rate_limit=True""" + mock_time.return_value = self.current_time + + client = Client(bearer_token=self.bearer_token, wait_on_rate_limit=True) + mock_response_429 = self.create_mock_429_response(include_reset_header=False) + mock_response_200 = Mock() + mock_response_200.status_code = 200 + + mock_response_429.__enter__ = Mock(return_value=mock_response_429) + mock_response_429.__exit__ = Mock(return_value=None) + mock_response_200.__enter__ = Mock(return_value=mock_response_200) + mock_response_200.__exit__ = Mock(return_value=None) + + with patch.object(client.session, 'request', side_effect=[mock_response_429, mock_response_200]): + result = client.request("GET", "/2/tweets/search/recent") + + # Should not have slept since no reset time available + mock_sleep.assert_not_called() + self.assertEqual(result, mock_response_200) + + # Async Client Tests + def test_async_client_rate_limit_with_reset_header_wait_false(self): + """Test async client raises TooManyRequests with reset_time when wait_on_rate_limit=False""" + async def run_test(): + client = AsyncClient(bearer_token=self.bearer_token, wait_on_rate_limit=False) + mock_response = self.create_mock_429_response(include_reset_header=True, sync=False) + + # Mock async context manager + mock_response.__aenter__ = AsyncMock(return_value=mock_response) + mock_response.__aexit__ = AsyncMock(return_value=None) + mock_response.read = AsyncMock() + + with patch('aiohttp.ClientSession.request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + await client.request("GET", "/2/tweets/search/recent") + + exception = cm.exception + self.assertEqual(exception.reset_time, self.reset_time) + + asyncio.run(run_test()) + + def test_async_client_rate_limit_without_reset_header_wait_false(self): + """Test async client raises TooManyRequests with None reset_time when header missing""" + async def run_test(): + client = AsyncClient(bearer_token=self.bearer_token, wait_on_rate_limit=False) + mock_response = self.create_mock_429_response(include_reset_header=False, sync=False) + + mock_response.__aenter__ = AsyncMock(return_value=mock_response) + mock_response.__aexit__ = AsyncMock(return_value=None) + mock_response.read = AsyncMock() + + with patch('aiohttp.ClientSession.request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + await client.request("GET", "/2/tweets/search/recent") + + exception = cm.exception + self.assertIsNone(exception.reset_time) + + asyncio.run(run_test()) + + def test_async_client_rate_limit_with_reset_header_wait_true(self): + """Test async client waits correctly when wait_on_rate_limit=True""" + async def run_test(): + with patch('asyncio.sleep') as mock_sleep, \ + patch('time.time', return_value=self.current_time): + + client = AsyncClient(bearer_token=self.bearer_token, wait_on_rate_limit=True) + mock_response_429 = self.create_mock_429_response(include_reset_header=True, sync=False) + mock_response_200 = Mock() + mock_response_200.status = 200 + + # Mock async context managers + mock_response_429.__aenter__ = AsyncMock(return_value=mock_response_429) + mock_response_429.__aexit__ = AsyncMock(return_value=None) + mock_response_429.read = AsyncMock() + + mock_response_200.__aenter__ = AsyncMock(return_value=mock_response_200) + mock_response_200.__aexit__ = AsyncMock(return_value=None) + mock_response_200.read = AsyncMock() + + with patch('aiohttp.ClientSession.request', side_effect=[mock_response_429, mock_response_200]): + result = await client.request("GET", "/2/tweets/search/recent") + + expected_sleep_time = self.reset_time - self.current_time + 1 + mock_sleep.assert_called_once_with(expected_sleep_time) + self.assertEqual(result, mock_response_200) + + asyncio.run(run_test()) + + # API v1 Client Tests + def test_api_v1_rate_limit_with_reset_time(self): + """Test API v1 client passes reset_time to TooManyRequests exception""" + # Create API with fake auth to bypass auth check + from tweepy.auth import OAuthHandler + auth = OAuthHandler(self.consumer_key, self.consumer_secret) + auth.set_access_token(self.access_token, self.access_token_secret) + + api = API(auth=auth, wait_on_rate_limit=False) + mock_response = Mock() + mock_response.status_code = 429 + mock_response.reason = "Too Many Requests" + mock_response.headers = {"x-rate-limit-reset": self.reset_time_str} + mock_response.json.return_value = {"errors": [{"message": "Rate limit exceeded"}]} + + with patch.object(api.session, 'request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + api.request("GET", "statuses/user_timeline") + + exception = cm.exception + self.assertEqual(exception.reset_time, self.reset_time) + + def test_api_v1_rate_limit_without_reset_time(self): + """Test API v1 client handles missing reset_time""" + # Create API with fake auth to bypass auth check + from tweepy.auth import OAuthHandler + auth = OAuthHandler(self.consumer_key, self.consumer_secret) + auth.set_access_token(self.access_token, self.access_token_secret) + + api = API(auth=auth, wait_on_rate_limit=False) + mock_response = Mock() + mock_response.status_code = 429 + mock_response.reason = "Too Many Requests" + mock_response.headers = {} # No reset time header + mock_response.json.return_value = {"errors": [{"message": "Rate limit exceeded"}]} + + with patch.object(api.session, 'request', return_value=mock_response): + with self.assertRaises(TooManyRequests) as cm: + api.request("GET", "statuses/user_timeline") + + exception = cm.exception + self.assertIsNone(exception.reset_time) + + +class RateLimitIntegrationTests(unittest.TestCase): + """Integration tests showing how applications can use the reset_time feature""" + + def test_application_can_handle_reset_time(self): + """Test that applications can access and use reset_time information""" + client = Client(bearer_token="fake_token", wait_on_rate_limit=False) + current_time = int(time.time()) + reset_time = current_time + 600 # 10 minutes from now + + mock_response = Mock() + mock_response.status_code = 429 + mock_response.reason = "Too Many Requests" + mock_response.headers = {"x-rate-limit-reset": str(reset_time)} + mock_response.json.return_value = {"errors": [{"message": "Rate limit exceeded"}]} + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=None) + + with patch.object(client.session, 'request', return_value=mock_response), \ + patch('time.time', return_value=current_time): + + try: + client.request("GET", "/2/tweets/search/recent") + self.fail("Should have raised TooManyRequests") + except TooManyRequests as e: + # Application can now implement custom rate limit handling + if e.reset_time: + sleep_time = e.reset_time - current_time + self.assertEqual(sleep_time, 600) + + # Application could show user-friendly message + import time as time_module + reset_time_str = time_module.ctime(e.reset_time) + self.assertIsInstance(reset_time_str, str) + + # Application could implement custom backoff + self.assertGreater(sleep_time, 0) + else: + self.fail("reset_time should be available") + + def test_backward_compatibility_maintained(self): + """Test that existing applications continue to work unchanged""" + client = Client(bearer_token="fake_token", wait_on_rate_limit=False) + + mock_response = Mock() + mock_response.status_code = 429 + mock_response.reason = "Too Many Requests" + mock_response.headers = {} # No reset header + mock_response.json.return_value = {"errors": [{"message": "Rate limit exceeded"}]} + mock_response.__enter__ = Mock(return_value=mock_response) + mock_response.__exit__ = Mock(return_value=None) + + with patch.object(client.session, 'request', return_value=mock_response): + # Old application code that doesn't know about reset_time + try: + client.request("GET", "/2/tweets/search/recent") + self.fail("Should have raised TooManyRequests") + except TooManyRequests as e: + # Should still work as before + self.assertIsInstance(e, TooManyRequests) + self.assertEqual(e.response.status_code, 429) + # New attribute should be None when not available + self.assertIsNone(e.reset_time) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/__init__.py new/tweepy-4.17.0/tweepy/__init__.py --- old/tweepy-4.16.0/tweepy/__init__.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/__init__.py 2026-07-02 21:08:52.000000000 +0200 @@ -5,7 +5,7 @@ """ Tweepy Twitter API library """ -__version__ = '4.16.0' +__version__ = '4.17.0' __author__ = 'Joshua Roesslein' __license__ = 'MIT' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/api.py new/tweepy-4.17.0/tweepy/api.py --- old/tweepy-4.16.0/tweepy/api.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/api.py 2026-07-02 21:08:52.000000000 +0200 @@ -271,7 +271,7 @@ if resp.status_code == 404: raise NotFound(resp) if resp.status_code == 429: - raise TooManyRequests(resp) + raise TooManyRequests(resp, reset_time=reset_time) if resp.status_code >= 500: raise TwitterServerError(resp) if resp.status_code and not 200 <= resp.status_code < 300: @@ -970,7 +970,7 @@ auto_populate_reply_metadata If set to true and used with in_reply_to_status_id, leading @mentions will be looked up from the original Tweet, and added to - the new Tweet from there. This wil append @mentions into the + the new Tweet from there. This will append @mentions into the metadata of an extended Tweet as a reply chain grows, until the limit on @mentions is reached. In cases where the original Tweet has been deleted, the reply will fail. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/asynchronous/client.py new/tweepy-4.17.0/tweepy/asynchronous/client.py --- old/tweepy-4.16.0/tweepy/asynchronous/client.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/asynchronous/client.py 2026-07-02 21:08:52.000000000 +0200 @@ -76,7 +76,7 @@ url, headers, body = oauth_client.sign( url, method, headers=headers ) - # oauthlib.oauth1.Client (OAuthClient) expects colons in query + # oauthlib.oauth1.Client (OAuthClient) expects colons in query # values (e.g. in timestamps) to be percent-encoded, while # aiohttp.ClientSession does not automatically encode them before_query, question_mark, query = url.partition('?') @@ -119,18 +119,22 @@ if response.status == 404: raise NotFound(response, response_json=response_json) if response.status == 429: - if self.wait_on_rate_limit: + reset_time = None + if "x-rate-limit-reset" in response.headers: reset_time = int(response.headers["x-rate-limit-reset"]) - sleep_time = reset_time - int(time.time()) + 1 - if sleep_time > 0: - log.warning( - "Rate limit exceeded. " - f"Sleeping for {sleep_time} seconds." - ) - await asyncio.sleep(sleep_time) + + if self.wait_on_rate_limit: + if reset_time is not None: + sleep_time = reset_time - int(time.time()) + 1 + if sleep_time > 0: + log.warning( + "Rate limit exceeded. " + f"Sleeping for {sleep_time} seconds." + ) + await asyncio.sleep(sleep_time) return await self.request(method, route, params, json, user_auth) else: - raise TooManyRequests(response, response_json=response_json) + raise TooManyRequests(response, response_json=response_json, reset_time=reset_time) if response.status >= 500: raise TwitterServerError(response, response_json=response_json) if not 200 <= response.status < 300: @@ -2520,7 +2524,7 @@ from the previous 30 days. .. note:: - + There is an alias for this method named ``get_dm_events``. .. versionadded:: 4.12 @@ -2531,14 +2535,14 @@ The ``id`` of the Direct Message conversation for which events are being retrieved. participant_id : int | str | None - The ``participant_id`` of the user that the authenicating user is + The ``participant_id`` of the user that the authenticating user is having a 1-1 conversation with. dm_event_fields : list[str] | str | None Extra fields to include in the event payload. ``id`` and ``event_type`` are returned by default. The ``text`` value isn't included for ``ParticipantsJoin`` and ``ParticipantsLeave`` events. event_types : str - The type of Direct Message event to returm. If not included, all + The type of Direct Message event to return. If not included, all types are returned. expansions : list[str] | str | None :ref:`expansions_parameter` @@ -2609,7 +2613,7 @@ adds the Direct Message to it. .. note:: - + There is an alias for this method named ``create_dm``. .. versionadded:: 4.12 @@ -2678,7 +2682,7 @@ behalf of the authenticated user. .. note:: - + There is an alias for this method named ``create_dm_conversation``. .. versionadded:: 4.12 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/cache.py new/tweepy-4.17.0/tweepy/cache.py --- old/tweepy-4.16.0/tweepy/cache.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/cache.py 2026-07-02 21:08:52.000000000 +0200 @@ -400,7 +400,7 @@ def store(self, key, value): from bson.binary import Binary - now = datetime.datetime.utcnow() + now = datetime.datetime.now(datetime.timezone.utc) blob = Binary(pickle.dumps(value)) self.col.insert({'created': now, '_id': key, 'value': blob}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/client.py new/tweepy-4.17.0/tweepy/client.py --- old/tweepy-4.16.0/tweepy/client.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/client.py 2026-07-02 21:08:52.000000000 +0200 @@ -101,18 +101,22 @@ if response.status_code == 404: raise NotFound(response) if response.status_code == 429: - if self.wait_on_rate_limit: + reset_time = None + if "x-rate-limit-reset" in response.headers: reset_time = int(response.headers["x-rate-limit-reset"]) - sleep_time = reset_time - int(time.time()) + 1 - if sleep_time > 0: - log.warning( - "Rate limit exceeded. " - f"Sleeping for {sleep_time} seconds." - ) - time.sleep(sleep_time) + + if self.wait_on_rate_limit: + if reset_time is not None: + sleep_time = reset_time - int(time.time()) + 1 + if sleep_time > 0: + log.warning( + "Rate limit exceeded. " + f"Sleeping for {sleep_time} seconds." + ) + time.sleep(sleep_time) return self.request(method, route, params, json, user_auth) else: - raise TooManyRequests(response) + raise TooManyRequests(response, reset_time=reset_time) if response.status_code >= 500: raise TwitterServerError(response) if not 200 <= response.status_code < 300: @@ -803,7 +807,7 @@ if community_id is not None: json["community_id"] = community_id - + if for_super_followers_only is not None: json["for_super_followers_only"] = for_super_followers_only @@ -2722,7 +2726,7 @@ from the previous 30 days. .. note:: - + There is an alias for this method named ``get_dm_events``. .. versionadded:: 4.12 @@ -2733,7 +2737,7 @@ The ``id`` of the Direct Message conversation for which events are being retrieved. participant_id : int | str | None - The ``participant_id`` of the user that the authenicating user is + The ``participant_id`` of the user that the authenticating user is having a 1-1 conversation with. dm_event_fields : list[str] | str | None Extra fields to include in the event payload. ``id`` and @@ -2811,7 +2815,7 @@ adds the Direct Message to it. .. note:: - + There is an alias for this method named ``create_dm``. .. versionadded:: 4.12 @@ -2878,7 +2882,7 @@ behalf of the authenticated user. .. note:: - + There is an alias for this method named ``create_dm_conversation``. .. versionadded:: 4.12 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/cursor.py new/tweepy-4.17.0/tweepy/cursor.py --- old/tweepy-4.16.0/tweepy/cursor.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/cursor.py 2026-07-02 21:08:52.000000000 +0200 @@ -188,7 +188,7 @@ if len(result) == 0: raise StopIteration - # TODO: Make this not dependant on the parser making max_id and + # TODO: Make this not dependent on the parser making max_id and # since_id available self.max_id = model.max_id self.num_tweets += 1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tweepy-4.16.0/tweepy/errors.py new/tweepy-4.17.0/tweepy/errors.py --- old/tweepy-4.16.0/tweepy/errors.py 2025-06-22 03:10:14.000000000 +0200 +++ new/tweepy-4.17.0/tweepy/errors.py 2026-07-02 21:08:52.000000000 +0200 @@ -138,8 +138,16 @@ Exception raised for a 429 HTTP status code .. versionadded:: 4.0 + + Attributes + ---------- + reset_time : int | None + Unix timestamp when the rate limit resets, if available """ - pass + + def __init__(self, response, *, response_json=None, reset_time=None): + super().__init__(response, response_json=response_json) + self.reset_time = reset_time class TwitterServerError(HTTPException):
