aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Pipfile1
-rw-r--r--Pipfile.lock8
-rw-r--r--docs/index.rst25
-rw-r--r--mastodon/Mastodon.py29
-rw-r--r--setup.py2
-rw-r--r--tests/test_errors.py20
-rw-r--r--tests/test_pagination.py20
7 files changed, 95 insertions, 10 deletions
diff --git a/Pipfile b/Pipfile
index 365589a..d0d3ce5 100644
--- a/Pipfile
+++ b/Pipfile
@@ -13,3 +13,4 @@ pytest-cov = "*"
13vcrpy = "*" 13vcrpy = "*"
14pytest-vcr = "<1" 14pytest-vcr = "<1"
15pytest-mock = "*" 15pytest-mock = "*"
16requests-mock = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index d1939fc..db1193b 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -313,6 +313,14 @@
313 ], 313 ],
314 "version": "==2.20.1" 314 "version": "==2.20.1"
315 }, 315 },
316 "requests-mock": {
317 "hashes": [
318 "sha256:7a5fa99db5e3a2a961b6f20ed40ee6baeff73503cf0a553cc4d679409e6170fb",
319 "sha256:8ca0628dc66d3f212878932fd741b02aa197ad53fd2228164800a169a4a826af"
320 ],
321 "index": "pypi",
322 "version": "==1.5.2"
323 },
316 "six": { 324 "six": {
317 "hashes": [ 325 "hashes": [
318 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 326 "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
diff --git a/docs/index.rst b/docs/index.rst
index 808e563..515e427 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -71,6 +71,26 @@ for applications that need to handle all rate limiting themselves (i.e. interact
71or applications wanting to use Mastodon.py in a multi-threaded context ("wait" and "pace" 71or applications wanting to use Mastodon.py in a multi-threaded context ("wait" and "pace"
72modes are not thread safe). 72modes are not thread safe).
73 73
74.. note::
75 Rate limit information is available on the `Mastodon` object for applications that
76 implement their own rate limit handling.
77
78 .. attribute:: Mastodon.ratelimit_remaining
79
80 Number of requests allowed until the next reset.
81
82 .. attribute:: Mastodon.ratelimit_reset
83
84 Time at which the rate limit will next be reset, as a POSIX timestamp.
85
86 .. attribute:: Mastodon.ratelimit_limit
87
88 Total number of requests allowed between resets. Typically 300.
89
90 .. attribute:: Mastodon.ratelimit_lastcall
91
92 Time at which these values have last been seen and updated, as a POSIX timestamp.
93
74In "wait" mode, once a request hits the rate limit, Mastodon.py will wait until 94In "wait" mode, once a request hits the rate limit, Mastodon.py will wait until
75the rate limit resets and then try again, until the request succeeds or an error 95the rate limit resets and then try again, until the request succeeds or an error
76is encountered. This mode is for applications that would rather just not worry about rate limits 96is encountered. This mode is for applications that would rather just not worry about rate limits
@@ -91,9 +111,8 @@ minute time slot, and tighter limits on logins. Mastodon.py does not make any ef
91to respect these. 111to respect these.
92 112
93If your application requires many hits to endpoints that are available without logging 113If your application requires many hits to endpoints that are available without logging
94in, do consider using Mastodon.py without authenticating to get the full per-IP limit. In 114in, do consider using Mastodon.py without authenticating to get the full per-IP limit.
95this case, you can set the Mastodon objects `ratelimit_limit` and `ratelimit_remaining` 115
96properties appropriately if you want to use advanced rate limit handling.
97 116
98A note about pagination 117A note about pagination
99----------------------- 118-----------------------
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index f78d303..dce2081 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -2222,9 +2222,12 @@ class Mastodon:
2222 if not response_object.ok: 2222 if not response_object.ok:
2223 try: 2223 try:
2224 response = response_object.json(object_hook=self.__json_hooks) 2224 response = response_object.json(object_hook=self.__json_hooks)
2225 if not isinstance(response, dict) or 'error' not in response: 2225 if isinstance(response, dict) and 'error' in response:
2226 error_msg = response['error']
2227 elif isinstance(response, str):
2228 error_msg = response
2229 else:
2226 error_msg = None 2230 error_msg = None
2227 error_msg = response['error']
2228 except ValueError: 2231 except ValueError:
2229 error_msg = None 2232 error_msg = None
2230 2233
@@ -2250,6 +2253,8 @@ class Mastodon:
2250 # on any 404 2253 # on any 404
2251 elif response_object.status_code == 401: 2254 elif response_object.status_code == 401:
2252 ex_type = MastodonUnauthorizedError 2255 ex_type = MastodonUnauthorizedError
2256 elif response_object.status_code == 502:
2257 ex_type = MastodonServerError
2253 else: 2258 else:
2254 ex_type = MastodonAPIError 2259 ex_type = MastodonAPIError
2255 2260
@@ -2280,13 +2285,17 @@ class Mastodon:
2280 if url['rel'] == 'next': 2285 if url['rel'] == 'next':
2281 # Be paranoid and extract max_id specifically 2286 # Be paranoid and extract max_id specifically
2282 next_url = url['url'] 2287 next_url = url['url']
2283 matchgroups = re.search(r"max_id=([0-9]*)", next_url) 2288 matchgroups = re.search(r"[?&]max_id=([^&]+)", next_url)
2284 2289
2285 if matchgroups: 2290 if matchgroups:
2286 next_params = copy.deepcopy(params) 2291 next_params = copy.deepcopy(params)
2287 next_params['_pagination_method'] = method 2292 next_params['_pagination_method'] = method
2288 next_params['_pagination_endpoint'] = endpoint 2293 next_params['_pagination_endpoint'] = endpoint
2289 next_params['max_id'] = int(matchgroups.group(1)) 2294 max_id = matchgroups.group(1)
2295 if max_id.isdigit():
2296 next_params['max_id'] = int(max_id)
2297 else:
2298 next_params['max_id'] = max_id
2290 if "since_id" in next_params: 2299 if "since_id" in next_params:
2291 del next_params['since_id'] 2300 del next_params['since_id']
2292 response[-1]._pagination_next = next_params 2301 response[-1]._pagination_next = next_params
@@ -2294,13 +2303,17 @@ class Mastodon:
2294 if url['rel'] == 'prev': 2303 if url['rel'] == 'prev':
2295 # Be paranoid and extract since_id specifically 2304 # Be paranoid and extract since_id specifically
2296 prev_url = url['url'] 2305 prev_url = url['url']
2297 matchgroups = re.search(r"since_id=([0-9]*)", prev_url) 2306 matchgroups = re.search(r"[?&]since_id=([^&]+)", prev_url)
2298 2307
2299 if matchgroups: 2308 if matchgroups:
2300 prev_params = copy.deepcopy(params) 2309 prev_params = copy.deepcopy(params)
2301 prev_params['_pagination_method'] = method 2310 prev_params['_pagination_method'] = method
2302 prev_params['_pagination_endpoint'] = endpoint 2311 prev_params['_pagination_endpoint'] = endpoint
2303 prev_params['since_id'] = int(matchgroups.group(1)) 2312 since_id = matchgroups.group(1)
2313 if since_id.isdigit():
2314 prev_params['since_id'] = int(since_id)
2315 else:
2316 prev_params['since_id'] = since_id
2304 if "max_id" in prev_params: 2317 if "max_id" in prev_params:
2305 del prev_params['max_id'] 2318 del prev_params['max_id']
2306 response[0]._pagination_prev = prev_params 2319 response[0]._pagination_prev = prev_params
@@ -2536,6 +2549,10 @@ class MastodonAPIError(MastodonError):
2536 """Raised when the mastodon API generates a response that cannot be handled""" 2549 """Raised when the mastodon API generates a response that cannot be handled"""
2537 pass 2550 pass
2538 2551
2552class MastodonServerError(MastodonError):
2553 """Raised if the Server is malconfigured, e.g. returns a 502 error code"""
2554 pass
2555
2539class MastodonNotFoundError(MastodonAPIError): 2556class MastodonNotFoundError(MastodonAPIError):
2540 """Raised when the mastodon API returns a 404 Not Found error""" 2557 """Raised when the mastodon API returns a 404 Not Found error"""
2541 pass 2558 pass
diff --git a/setup.py b/setup.py
index 009063d..72e3c8b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
1from setuptools import setup 1from setuptools import setup
2 2
3test_deps = ['pytest', 'pytest-runner', 'pytest-cov', 'vcrpy', 'pytest-vcr', 'pytest-mock'] 3test_deps = ['pytest', 'pytest-runner', 'pytest-cov', 'vcrpy', 'pytest-vcr', 'pytest-mock', 'requests-mock']
4extras = { 4extras = {
5 "test": test_deps 5 "test": test_deps
6} 6}
diff --git a/tests/test_errors.py b/tests/test_errors.py
new file mode 100644
index 0000000..7329507
--- /dev/null
+++ b/tests/test_errors.py
@@ -0,0 +1,20 @@
1import pytest
2from mastodon.Mastodon import MastodonAPIError
3
4try:
5 from mock import MagicMock
6except ImportError:
7 from unittest.mock import MagicMock
8
9def test_nonstandard_errors(api):
10 response = MagicMock()
11 response.json = MagicMock(return_value=
12 "I am a non-standard instance and this error is a plain string.")
13 response.ok = False
14 session = MagicMock()
15 session.request = MagicMock(return_value=response)
16
17 api.session = session
18 with pytest.raises(MastodonAPIError):
19 api.instance()
20
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 599b2f4..d2c0bd5 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -1,5 +1,10 @@
1import pytest 1import pytest
2from contextlib import contextmanager 2from contextlib import contextmanager
3try:
4 from mock import MagicMock
5except ImportError:
6 from unittest.mock import MagicMock
7import requests_mock
3 8
4UNLIKELY_HASHTAG = "fgiztsshwiaqqiztpmmjbtvmescsculuvmgjgopwoeidbcrixp" 9UNLIKELY_HASHTAG = "fgiztsshwiaqqiztpmmjbtvmescsculuvmgjgopwoeidbcrixp"
5 10
@@ -44,3 +49,18 @@ def test_fetch_remaining(api):
44 hashtag_remaining = api.fetch_remaining(hashtag) 49 hashtag_remaining = api.fetch_remaining(hashtag)
45 assert hashtag_remaining 50 assert hashtag_remaining
46 assert len(hashtag_remaining) >= 30 51 assert len(hashtag_remaining) >= 30
52
53def test_link_headers(api):
54 rmock = requests_mock.Adapter()
55 api.session.mount(api.api_base_url, rmock)
56
57 _id='abc1234'
58
59 rmock.register_uri('GET', requests_mock.ANY, json=[{"foo": "bar"}], headers={"link":"""
60 <{base}/api/v1/timelines/tag/{tag}?max_id={_id}>; rel="next", <{base}/api/v1/timelines/tag/{tag}?since_id={_id}>; rel="prev"
61 """.format(base=api.api_base_url, tag=UNLIKELY_HASHTAG, _id=_id).strip()
62 })
63
64 resp = api.timeline_hashtag(UNLIKELY_HASHTAG)
65 assert resp[0]._pagination_next['max_id'] == _id
66 assert resp[0]._pagination_prev['since_id'] == _id
Powered by cgit v1.2.3 (git 2.41.0)