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

Reply via email to