diff options
author | Lorenz Diener <[email protected]> | 2018-01-29 11:18:37 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2018-01-29 11:18:37 +0100 |
commit | 42b1d8fa5895f416853311db220cf9412ad8fd13 (patch) | |
tree | cc8615955cba00aa7ba224733e3371379bbfb855 | |
parent | 1d54c35101a6c349e457e7b708e29db15242a139 (diff) | |
parent | 56e6bac9cb19101346cb42b73628290740889ecf (diff) | |
download | mastodon.py-42b1d8fa5895f416853311db220cf9412ad8fd13.tar.gz |
Merge pull request #118 from codl/subclass-api-errors
Subclass api errors
-rw-r--r-- | mastodon/Mastodon.py | 78 | ||||
-rw-r--r-- | tests/cassettes/test_unauthed_home_tl_throws.yaml | 82 | ||||
-rw-r--r-- | tests/test_status.py | 5 | ||||
-rw-r--r-- | tests/test_timeline.py | 9 |
4 files changed, 139 insertions, 35 deletions
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py index 28ad9d8..5aca185 100644 --- a/mastodon/Mastodon.py +++ b/mastodon/Mastodon.py | |||
@@ -1550,33 +1550,45 @@ class Mastodon: | |||
1550 | print('response headers: ' + str(response_object.headers)) | 1550 | print('response headers: ' + str(response_object.headers)) |
1551 | print('Response text content: ' + str(response_object.text)) | 1551 | print('Response text content: ' + str(response_object.text)) |
1552 | 1552 | ||
1553 | if response_object.status_code == 404: | 1553 | if not response_object.ok: |
1554 | try: | 1554 | try: |
1555 | response = response_object.json() | 1555 | response = response_object.json(object_hook=self.__json_hooks) |
1556 | except: | 1556 | if not isinstance(response, dict) or 'error' not in response: |
1557 | raise MastodonAPIError('Endpoint not found.') | 1557 | error_msg = None |
1558 | 1558 | error_msg = response['error'] | |
1559 | if isinstance(response, dict) and 'error' in response: | 1559 | except ValueError: |
1560 | raise MastodonAPIError("Mastodon API returned error: " + str(response['error'])) | 1560 | error_msg = None |
1561 | |||
1562 | # Handle rate limiting | ||
1563 | if response_object.status_code == 429: | ||
1564 | if self.ratelimit_method == 'throw' or not do_ratelimiting: | ||
1565 | raise MastodonRatelimitError('Hit rate limit.') | ||
1566 | elif self.ratelimit_method in ('wait', 'pace'): | ||
1567 | to_next = self.ratelimit_reset - time.time() | ||
1568 | if to_next > 0: | ||
1569 | # As a precaution, never sleep longer than 5 minutes | ||
1570 | to_next = min(to_next, 5 * 60) | ||
1571 | time.sleep(to_next) | ||
1572 | request_complete = False | ||
1573 | continue | ||
1574 | |||
1575 | if response_object.status_code == 404: | ||
1576 | ex_type = MastodonNotFoundError | ||
1577 | if not error_msg: | ||
1578 | error_msg = 'Endpoint not found.' | ||
1579 | # this is for compatibility with older versions | ||
1580 | # which raised MastodonAPIError('Endpoint not found.') | ||
1581 | # on any 404 | ||
1582 | elif response_object.status_code == 401: | ||
1583 | ex_type = MastodonUnauthorizedError | ||
1561 | else: | 1584 | else: |
1562 | raise MastodonAPIError('Endpoint not found.') | 1585 | ex_type = MastodonAPIError |
1563 | 1586 | ||
1564 | 1587 | raise ex_type( | |
1565 | if response_object.status_code == 500: | 1588 | 'Mastodon API returned error', |
1566 | raise MastodonAPIError('General API problem.') | 1589 | response_object.status_code, |
1567 | 1590 | response_object.reason, | |
1568 | # Handle rate limiting | 1591 | error_msg) |
1569 | if response_object.status_code == 429: | ||
1570 | if self.ratelimit_method == 'throw' or not do_ratelimiting: | ||
1571 | raise MastodonRatelimitError('Hit rate limit.') | ||
1572 | elif self.ratelimit_method in ('wait', 'pace'): | ||
1573 | to_next = self.ratelimit_reset - time.time() | ||
1574 | if to_next > 0: | ||
1575 | # As a precaution, never sleep longer than 5 minutes | ||
1576 | to_next = min(to_next, 5 * 60) | ||
1577 | time.sleep(to_next) | ||
1578 | request_complete = False | ||
1579 | continue | ||
1580 | 1592 | ||
1581 | try: | 1593 | try: |
1582 | response = response_object.json(object_hook=self.__json_hooks) | 1594 | response = response_object.json(object_hook=self.__json_hooks) |
@@ -1586,12 +1598,6 @@ class Mastodon: | |||
1586 | "bad json content was '%s'" % (response_object.status_code, | 1598 | "bad json content was '%s'" % (response_object.status_code, |
1587 | response_object.content)) | 1599 | response_object.content)) |
1588 | 1600 | ||
1589 | # See if the returned dict is an error dict even though status is 200 | ||
1590 | if isinstance(response, dict) and 'error' in response: | ||
1591 | if not isinstance(response['error'], six.string_types): | ||
1592 | response['error'] = six.text_type(response['error']) | ||
1593 | raise MastodonAPIError("Mastodon API returned error: " + response['error']) | ||
1594 | |||
1595 | # Parse link headers | 1601 | # Parse link headers |
1596 | if isinstance(response, list) and \ | 1602 | if isinstance(response, list) and \ |
1597 | 'Link' in response_object.headers and \ | 1603 | 'Link' in response_object.headers and \ |
@@ -1801,6 +1807,16 @@ class MastodonAPIError(MastodonError): | |||
1801 | """Raised when the mastodon API generates a response that cannot be handled""" | 1807 | """Raised when the mastodon API generates a response that cannot be handled""" |
1802 | pass | 1808 | pass |
1803 | 1809 | ||
1810 | class MastodonNotFoundError(MastodonAPIError): | ||
1811 | """Raised when the mastodon API returns a 404 Not Found error""" | ||
1812 | pass | ||
1813 | |||
1814 | class MastodonUnauthorizedError(MastodonAPIError): | ||
1815 | """Raised when the mastodon API returns a 401 Unauthorized error | ||
1816 | |||
1817 | This happens when an OAuth token is invalid or has been revoked.""" | ||
1818 | pass | ||
1819 | |||
1804 | 1820 | ||
1805 | class MastodonRatelimitError(MastodonError): | 1821 | class MastodonRatelimitError(MastodonError): |
1806 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" | 1822 | """Raised when rate limiting is set to manual mode and the rate limit is exceeded""" |
diff --git a/tests/cassettes/test_unauthed_home_tl_throws.yaml b/tests/cassettes/test_unauthed_home_tl_throws.yaml new file mode 100644 index 0000000..b63d840 --- /dev/null +++ b/tests/cassettes/test_unauthed_home_tl_throws.yaml | |||
@@ -0,0 +1,82 @@ | |||
1 | interactions: | ||
2 | - request: | ||
3 | body: visibility=&status=Toot%21 | ||
4 | headers: | ||
5 | Accept: ['*/*'] | ||
6 | Accept-Encoding: ['gzip, deflate'] | ||
7 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
8 | Connection: [keep-alive] | ||
9 | Content-Length: ['26'] | ||
10 | Content-Type: [application/x-www-form-urlencoded] | ||
11 | User-Agent: [python-requests/2.18.4] | ||
12 | method: POST | ||
13 | uri: http://localhost:3000/api/v1/statuses | ||
14 | response: | ||
15 | body: {string: '{"id":"99285482671609362","created_at":"2018-01-03T10:43:57.160Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"private","language":"ja","uri":"http://localhost:3000/users/mastodonpy_test/statuses/99285482671609362","content":"\u003cp\u003eToot!\u003c/p\u003e","url":"http://localhost:3000/@mastodonpy_test/99285482671609362","reblogs_count":0,"favourites_count":0,"favourited":false,"reblogged":false,"muted":false,"reblog":null,"application":{"name":"Mastodon.py | ||
16 | test suite","website":null},"account":{"id":"1234567890123456","username":"mastodonpy_test","acct":"mastodonpy_test","display_name":"","locked":true,"created_at":"2018-01-03T11:24:32.957Z","note":"\u003cp\u003e\u003c/p\u003e","url":"http://localhost:3000/@mastodonpy_test","avatar":"http://localhost:3000/avatars/original/missing.png","avatar_static":"http://localhost:3000/avatars/original/missing.png","header":"http://localhost:3000/headers/original/missing.png","header_static":"http://localhost:3000/headers/original/missing.png","followers_count":0,"following_count":0,"statuses_count":1},"media_attachments":[],"mentions":[],"tags":[],"emojis":[]}'} | ||
17 | headers: | ||
18 | Cache-Control: ['max-age=0, private, must-revalidate'] | ||
19 | Content-Type: [application/json; charset=utf-8] | ||
20 | ETag: [W/"d9b57bb0592371b00e98fbc0f44a8fc9"] | ||
21 | Transfer-Encoding: [chunked] | ||
22 | Vary: ['Accept-Encoding, Origin'] | ||
23 | X-Content-Type-Options: [nosniff] | ||
24 | X-Frame-Options: [SAMEORIGIN] | ||
25 | X-Request-Id: [d7a9df07-1a3c-4784-adc5-b67bd6347614] | ||
26 | X-Runtime: ['0.301984'] | ||
27 | X-XSS-Protection: [1; mode=block] | ||
28 | content-length: ['1175'] | ||
29 | status: {code: 200, message: OK} | ||
30 | - request: | ||
31 | body: null | ||
32 | headers: | ||
33 | Accept: ['*/*'] | ||
34 | Accept-Encoding: ['gzip, deflate'] | ||
35 | Connection: [keep-alive] | ||
36 | User-Agent: [python-requests/2.18.4] | ||
37 | method: GET | ||
38 | uri: http://localhost:3000/api/v1/timelines/home | ||
39 | response: | ||
40 | body: {string: '{"error":"The access token is invalid"}'} | ||
41 | headers: | ||
42 | Cache-Control: [no-store] | ||
43 | Content-Type: [application/json; charset=utf-8] | ||
44 | Pragma: [no-cache] | ||
45 | Transfer-Encoding: [chunked] | ||
46 | Vary: ['Accept-Encoding, Origin'] | ||
47 | WWW-Authenticate: ['Bearer realm="Doorkeeper", error="invalid_token", error_description="The | ||
48 | access token is invalid"'] | ||
49 | X-Content-Type-Options: [nosniff] | ||
50 | X-Frame-Options: [SAMEORIGIN] | ||
51 | X-Request-Id: [dc45d4f4-c203-4b28-ad27-f0db32912a16] | ||
52 | X-Runtime: ['0.010224'] | ||
53 | X-XSS-Protection: [1; mode=block] | ||
54 | content-length: ['39'] | ||
55 | status: {code: 401, message: Unauthorized} | ||
56 | - request: | ||
57 | body: null | ||
58 | headers: | ||
59 | Accept: ['*/*'] | ||
60 | Accept-Encoding: ['gzip, deflate'] | ||
61 | Authorization: [Bearer __MASTODON_PY_TEST_ACCESS_TOKEN] | ||
62 | Connection: [keep-alive] | ||
63 | Content-Length: ['0'] | ||
64 | User-Agent: [python-requests/2.18.4] | ||
65 | method: DELETE | ||
66 | uri: http://localhost:3000/api/v1/statuses/99285482671609362 | ||
67 | response: | ||
68 | body: {string: '{}'} | ||
69 | headers: | ||
70 | Cache-Control: ['max-age=0, private, must-revalidate'] | ||
71 | Content-Type: [application/json; charset=utf-8] | ||
72 | ETag: [W/"8ca371aea536ee2c56c8d13b43824703"] | ||
73 | Transfer-Encoding: [chunked] | ||
74 | Vary: ['Accept-Encoding, Origin'] | ||
75 | X-Content-Type-Options: [nosniff] | ||
76 | X-Frame-Options: [SAMEORIGIN] | ||
77 | X-Request-Id: [ddbd4335-1aeb-42af-8dea-fa78a787609f] | ||
78 | X-Runtime: ['0.017701'] | ||
79 | X-XSS-Protection: [1; mode=block] | ||
80 | content-length: ['2'] | ||
81 | status: {code: 200, message: OK} | ||
82 | version: 1 | ||
diff --git a/tests/test_status.py b/tests/test_status.py index b177517..2e129ac 100644 --- a/tests/test_status.py +++ b/tests/test_status.py | |||
@@ -1,6 +1,5 @@ | |||
1 | import pytest | 1 | import pytest |
2 | from mastodon.Mastodon import MastodonAPIError | 2 | from mastodon.Mastodon import MastodonAPIError, MastodonNotFoundError |
3 | from time import sleep | ||
4 | 3 | ||
5 | @pytest.mark.vcr() | 4 | @pytest.mark.vcr() |
6 | def test_status(status, api): | 5 | def test_status(status, api): |
@@ -14,7 +13,7 @@ def test_status_empty(api): | |||
14 | 13 | ||
15 | @pytest.mark.vcr() | 14 | @pytest.mark.vcr() |
16 | def test_status_missing(api): | 15 | def test_status_missing(api): |
17 | with pytest.raises(MastodonAPIError): | 16 | with pytest.raises(MastodonNotFoundError): |
18 | api.status(0) | 17 | api.status(0) |
19 | 18 | ||
20 | @pytest.mark.skip(reason="Doesn't look like mastodon will make a card for an url that doesn't have a TLD, and relying on some external website being reachable to make a card of is messy :/") | 19 | @pytest.mark.skip(reason="Doesn't look like mastodon will make a card for an url that doesn't have a TLD, and relying on some external website being reachable to make a card of is messy :/") |
diff --git a/tests/test_timeline.py b/tests/test_timeline.py index 6a27be3..5108b63 100644 --- a/tests/test_timeline.py +++ b/tests/test_timeline.py | |||
@@ -1,5 +1,7 @@ | |||
1 | import pytest | 1 | import pytest |
2 | from mastodon.Mastodon import MastodonAPIError, MastodonIllegalArgumentError | 2 | from mastodon.Mastodon import MastodonAPIError,\ |
3 | MastodonIllegalArgumentError,\ | ||
4 | MastodonUnauthorizedError | ||
3 | 5 | ||
4 | @pytest.mark.vcr() | 6 | @pytest.mark.vcr() |
5 | def test_public_tl_anonymous(api_anonymous, status): | 7 | def test_public_tl_anonymous(api_anonymous, status): |
@@ -18,6 +20,11 @@ def test_public_tl(api, status): | |||
18 | assert status['id'] in map(lambda st: st['id'], local) | 20 | assert status['id'] in map(lambda st: st['id'], local) |
19 | 21 | ||
20 | @pytest.mark.vcr() | 22 | @pytest.mark.vcr() |
23 | def test_unauthed_home_tl_throws(api_anonymous, status): | ||
24 | with pytest.raises(MastodonUnauthorizedError): | ||
25 | api_anonymous.timeline_home() | ||
26 | |||
27 | @pytest.mark.vcr() | ||
21 | def test_home_tl(api, status): | 28 | def test_home_tl(api, status): |
22 | tl = api.timeline_home() | 29 | tl = api.timeline_home() |
23 | assert status['id'] in map(lambda st: st['id'], tl) | 30 | assert status['id'] in map(lambda st: st['id'], tl) |