aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFoxMaSk <[email protected]>2017-09-05 22:59:32 +0200
committerFoxMaSk <[email protected]>2017-09-05 22:59:32 +0200
commite0e68ccd6a8007fa9fb50aa7e8432505dc93f7b8 (patch)
treeb8fdb2f7c5d7317fd4dd4e97853cfbcb963e66bb
parentc8490be2a717467c61381245e0fa2793a48c57ae (diff)
downloadmastodon.py-e0e68ccd6a8007fa9fb50aa7e8432505dc93f7b8.tar.gz
not pep8 compliant #71
-rw-r--r--docs/conf.py4
-rw-r--r--mastodon/Mastodon.py335
-rw-r--r--mastodon/streaming.py29
-rw-r--r--setup.py7
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/test_streaming.py17
6 files changed, 218 insertions, 174 deletions
diff --git a/docs/conf.py b/docs/conf.py
index 9c4a292..4c5352a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -30,7 +30,7 @@ import os
30import sys 30import sys
31sys.path.insert(0, os.path.abspath('../')) 31sys.path.insert(0, os.path.abspath('../'))
32autodoc_member_order = 'by_source' 32autodoc_member_order = 'by_source'
33#print(sys.path) 33# print(sys.path)
34 34
35# Add any Sphinx extension module names here, as strings. They can be 35# Add any Sphinx extension module names here, as strings. They can be
36# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@@ -154,7 +154,7 @@ todo_include_todos = False
154# html_logo = None 154# html_logo = None
155 155
156# The name of an image file (relative to this directory) to use as a favicon of 156# The name of an image file (relative to this directory) to use as a favicon of
157# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 157# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
158# pixels large. 158# pixels large.
159# 159#
160# html_favicon = None 160# html_favicon = None
diff --git a/mastodon/Mastodon.py b/mastodon/Mastodon.py
index c835bac..c8f90ee 100644
--- a/mastodon/Mastodon.py
+++ b/mastodon/Mastodon.py
@@ -6,7 +6,6 @@ import mimetypes
6import time 6import time
7import random 7import random
8import string 8import string
9import pytz
10import datetime 9import datetime
11from contextlib import closing 10from contextlib import closing
12import pytz 11import pytz
@@ -17,6 +16,7 @@ import dateutil.parser
17import re 16import re
18import copy 17import copy
19 18
19
20class Mastodon: 20class Mastodon:
21 """ 21 """
22 Super basic but thorough and easy to use Mastodon 22 Super basic but thorough and easy to use Mastodon
@@ -28,12 +28,12 @@ class Mastodon:
28 __DEFAULT_BASE_URL = 'https://mastodon.social' 28 __DEFAULT_BASE_URL = 'https://mastodon.social'
29 __DEFAULT_TIMEOUT = 300 29 __DEFAULT_TIMEOUT = 300
30 30
31
32 ### 31 ###
33 # Registering apps 32 # Registering apps
34 ### 33 ###
35 @staticmethod 34 @staticmethod
36 def create_app(client_name, scopes = ['read', 'write', 'follow'], redirect_uris = None, website = None, to_file = None, api_base_url = __DEFAULT_BASE_URL, request_timeout = __DEFAULT_TIMEOUT): 35 def create_app(client_name, scopes=['read', 'write', 'follow'], redirect_uris=None, website=None, to_file=None,
36 api_base_url=__DEFAULT_BASE_URL, request_timeout=__DEFAULT_TIMEOUT):
37 """ 37 """
38 Create a new app with given client_name and scopes (read, write, follow) 38 Create a new app with given client_name and scopes (read, write, follow)
39 39
@@ -47,7 +47,7 @@ class Mastodon:
47 Returns client_id and client_secret. 47 Returns client_id and client_secret.
48 """ 48 """
49 api_base_url = Mastodon.__protocolize(api_base_url) 49 api_base_url = Mastodon.__protocolize(api_base_url)
50 50
51 request_data = { 51 request_data = {
52 'client_name': client_name, 52 'client_name': client_name,
53 'scopes': " ".join(scopes) 53 'scopes': " ".join(scopes)
@@ -55,18 +55,18 @@ class Mastodon:
55 55
56 try: 56 try:
57 if redirect_uris is not None: 57 if redirect_uris is not None:
58 request_data['redirect_uris'] = redirect_uris; 58 request_data['redirect_uris'] = redirect_uris
59 else: 59 else:
60 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'; 60 request_data['redirect_uris'] = 'urn:ietf:wg:oauth:2.0:oob'
61 if website is not None: 61 if website is not None:
62 request_data['website'] = website 62 request_data['website'] = website
63 63
64 response = requests.post(api_base_url + '/api/v1/apps', data = request_data, timeout = request_timeout) 64 response = requests.post(api_base_url + '/api/v1/apps', data=request_data, timeout=request_timeout)
65 response = response.json() 65 response = response.json()
66 except Exception as e: 66 except Exception as e:
67 raise MastodonNetworkError("Could not complete request: %s" % e) 67 raise MastodonNetworkError("Could not complete request: %s" % e)
68 68
69 if to_file != None: 69 if to_file is not None:
70 with open(to_file, 'w') as secret_file: 70 with open(to_file, 'w') as secret_file:
71 secret_file.write(response['client_id'] + '\n') 71 secret_file.write(response['client_id'] + '\n')
72 secret_file.write(response['client_secret'] + '\n') 72 secret_file.write(response['client_secret'] + '\n')
@@ -76,7 +76,9 @@ class Mastodon:
76 ### 76 ###
77 # Authentication, including constructor 77 # Authentication, including constructor
78 ### 78 ###
79 def __init__(self, client_id, client_secret = None, access_token = None, api_base_url = __DEFAULT_BASE_URL, debug_requests = False, ratelimit_method = "wait", ratelimit_pacefactor = 1.1, request_timeout = __DEFAULT_TIMEOUT): 79 def __init__(self, client_id, client_secret=None, access_token=None, api_base_url=__DEFAULT_BASE_URL,
80 debug_requests=False, ratelimit_method="wait", ratelimit_pacefactor=1.1,
81 request_timeout=__DEFAULT_TIMEOUT):
80 """ 82 """
81 Create a new API wrapper instance based on the given client_secret and client_id. If you 83 Create a new API wrapper instance based on the given client_secret and client_id. If you
82 give a client_id and it is not a file, you must also give a secret. 84 give a client_id and it is not a file, you must also give a secret.
@@ -115,7 +117,7 @@ class Mastodon:
115 117
116 self.request_timeout = request_timeout 118 self.request_timeout = request_timeout
117 119
118 if not ratelimit_method in ["throw", "wait", "pace"]: 120 if ratelimit_method not in ["throw", "wait", "pace"]:
119 raise MastodonIllegalArgumentError("Invalid ratelimit method.") 121 raise MastodonIllegalArgumentError("Invalid ratelimit method.")
120 122
121 if os.path.isfile(self.client_id): 123 if os.path.isfile(self.client_id):
@@ -123,15 +125,15 @@ class Mastodon:
123 self.client_id = secret_file.readline().rstrip() 125 self.client_id = secret_file.readline().rstrip()
124 self.client_secret = secret_file.readline().rstrip() 126 self.client_secret = secret_file.readline().rstrip()
125 else: 127 else:
126 if self.client_secret == None: 128 if self.client_secret is None:
127 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret') 129 raise MastodonIllegalArgumentError('Specified client id directly, but did not supply secret')
128 130
129 if self.access_token != None and os.path.isfile(self.access_token): 131 if self.access_token is not None and os.path.isfile(self.access_token):
130 with open(self.access_token, 'r') as token_file: 132 with open(self.access_token, 'r') as token_file:
131 self.access_token = token_file.readline().rstrip() 133 self.access_token = token_file.readline().rstrip()
132
133 134
134 def auth_request_url(self, client_id = None, redirect_uris = "urn:ietf:wg:oauth:2.0:oob", scopes = ['read', 'write', 'follow']): 135 def auth_request_url(self, client_id=None, redirect_uris="urn:ietf:wg:oauth:2.0:oob",
136 scopes=['read', 'write', 'follow']):
135 """Returns the url that a client needs to request the grant from the server. 137 """Returns the url that a client needs to request the grant from the server.
136 """ 138 """
137 if client_id is None: 139 if client_id is None:
@@ -140,8 +142,8 @@ class Mastodon:
140 if os.path.isfile(client_id): 142 if os.path.isfile(client_id):
141 with open(client_id, 'r') as secret_file: 143 with open(client_id, 'r') as secret_file:
142 client_id = secret_file.readline().rstrip() 144 client_id = secret_file.readline().rstrip()
143 145
144 params = {} 146 params = dict()
145 params['client_id'] = client_id 147 params['client_id'] = client_id
146 params['response_type'] = "code" 148 params['response_type'] = "code"
147 params['redirect_uri'] = redirect_uris 149 params['redirect_uri'] = redirect_uris
@@ -149,18 +151,18 @@ class Mastodon:
149 formatted_params = urlencode(params) 151 formatted_params = urlencode(params)
150 return "".join([self.api_base_url, "/oauth/authorize?", formatted_params]) 152 return "".join([self.api_base_url, "/oauth/authorize?", formatted_params])
151 153
152 def log_in(self, username = None, password = None,\ 154 def log_in(self, username=None, password=None,
153 code = None, redirect_uri = "urn:ietf:wg:oauth:2.0:oob", refresh_token = None,\ 155 code=None, redirect_uri="urn:ietf:wg:oauth:2.0:oob", refresh_token=None,
154 scopes = ['read', 'write', 'follow'], to_file = None): 156 scopes=['read', 'write', 'follow'], to_file=None):
155 """ 157 """
156 Your username is the e-mail you use to log in into mastodon. 158 Your username is the e-mail you use to log in into mastodon.
157 159
158 Can persist access token to file, to be used in the constructor. 160 Can persist access token to file, to be used in the constructor.
159 161
160 Supports refresh_token but Mastodon.social doesn't implement it at the moment. 162 Supports refresh_token but Mastodon.social doesn't implement it at the moment.
161 163
162 Handles password, authorization_code, and refresh_token authentication. 164 Handles password, authorization_code, and refresh_token authentication.
163 165
164 Will throw a MastodonIllegalArgumentError if username / password 166 Will throw a MastodonIllegalArgumentError if username / password
165 are wrong, scopes are not valid or granted scopes differ from requested. 167 are wrong, scopes are not valid or granted scopes differ from requested.
166 168
@@ -179,13 +181,13 @@ class Mastodon:
179 params['grant_type'] = 'refresh_token' 181 params['grant_type'] = 'refresh_token'
180 else: 182 else:
181 raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.') 183 raise MastodonIllegalArgumentError('Invalid arguments given. username and password or code are required.')
182 184
183 params['client_id'] = self.client_id 185 params['client_id'] = self.client_id
184 params['client_secret'] = self.client_secret 186 params['client_secret'] = self.client_secret
185 params['scope'] = " ".join(scopes) 187 params['scope'] = " ".join(scopes)
186 188
187 try: 189 try:
188 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting = False) 190 response = self.__api_request('POST', '/oauth/token', params, do_ratelimiting=False)
189 self.access_token = response['access_token'] 191 self.access_token = response['access_token']
190 self.__set_refresh_token(response.get('refresh_token')) 192 self.__set_refresh_token(response.get('refresh_token'))
191 self.__set_token_expired(int(response.get('expires_in', 0))) 193 self.__set_token_expired(int(response.get('expires_in', 0)))
@@ -201,9 +203,10 @@ class Mastodon:
201 received_scopes = " ".join(sorted(response["scope"].split(" "))) 203 received_scopes = " ".join(sorted(response["scope"].split(" ")))
202 204
203 if requested_scopes != received_scopes: 205 if requested_scopes != received_scopes:
204 raise MastodonAPIError('Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".') 206 raise MastodonAPIError(
207 'Granted scopes "' + received_scopes + '" differ from requested scopes "' + requested_scopes + '".')
205 208
206 if to_file != None: 209 if to_file is not None:
207 with open(to_file, 'w') as token_file: 210 with open(to_file, 'w') as token_file:
208 token_file.write(response['access_token'] + '\n') 211 token_file.write(response['access_token'] + '\n')
209 212
@@ -223,7 +226,7 @@ class Mastodon:
223 ### 226 ###
224 # Reading data: Timelines 227 # Reading data: Timelines
225 ## 228 ##
226 def timeline(self, timeline = "home", max_id = None, since_id = None, limit = None): 229 def timeline(self, timeline="home", max_id=None, since_id=None, limit=None):
227 """ 230 """
228 Fetch statuses, most recent ones first. Timeline can be home, local, public, 231 Fetch statuses, most recent ones first. Timeline can be home, local, public,
229 or tag/hashtag. See the following functions documentation for what those do. 232 or tag/hashtag. See the following functions documentation for what those do.
@@ -239,39 +242,44 @@ class Mastodon:
239 params_initial['local'] = True 242 params_initial['local'] = True
240 243
241 params = self.__generate_params(params_initial, ['timeline']) 244 params = self.__generate_params(params_initial, ['timeline'])
242 return self.__api_request('GET', '/api/v1/timelines/' + timeline, params) 245 url = '/api/v1/timelines/{0}'.format(timeline)
246 return self.__api_request('GET', url, params)
243 247
244 def timeline_home(self, max_id = None, since_id = None, limit = None): 248 def timeline_home(self, max_id=None, since_id=None, limit=None):
245 """ 249 """
246 Fetch the authenticated users home timeline (i.e. followed users and self). 250 Fetch the authenticated users home timeline (i.e. followed users and self).
247 251
248 Returns a list of toot dicts. 252 Returns a list of toot dicts.
249 """ 253 """
250 return self.timeline('home', max_id = max_id, since_id = since_id, limit = limit) 254 return self.timeline('home', max_id=max_id, since_id=since_id,
255 limit=limit)
251 256
252 def timeline_local(self, max_id = None, since_id = None, limit = None): 257 def timeline_local(self, max_id=None, since_id=None, limit=None):
253 """ 258 """
254 Fetches the local / instance-wide timeline, not including replies. 259 Fetches the local / instance-wide timeline, not including replies.
255 260
256 Returns a list of toot dicts. 261 Returns a list of toot dicts.
257 """ 262 """
258 return self.timeline('local', max_id = max_id, since_id = since_id, limit = limit) 263 return self.timeline('local', max_id=max_id, since_id=since_id,
264 limit=limit)
259 265
260 def timeline_public(self, max_id = None, since_id = None, limit = None): 266 def timeline_public(self, max_id=None, since_id=None, limit=None):
261 """ 267 """
262 Fetches the public / visible-network timeline, not including replies. 268 Fetches the public / visible-network timeline, not including replies.
263 269
264 Returns a list of toot dicts. 270 Returns a list of toot dicts.
265 """ 271 """
266 return self.timeline('public', max_id = max_id, since_id = since_id, limit = limit) 272 return self.timeline('public', max_id=max_id, since_id=since_id,
273 limit=limit)
267 274
268 def timeline_hashtag(self, hashtag, max_id = None, since_id = None, limit = None): 275 def timeline_hashtag(self, hashtag, max_id=None, since_id=None, limit=None):
269 """ 276 """
270 Fetch a timeline of toots with a given hashtag. 277 Fetch a timeline of toots with a given hashtag.
271 278
272 Returns a list of toot dicts. 279 Returns a list of toot dicts.
273 """ 280 """
274 return self.timeline('tag/' + str(hashtag), max_id = max_id, since_id = since_id, limit = limit) 281 url = 'tag/{0}'.format(str(hashtag))
282 return self.timeline(url, max_id=max_id, since_id=since_id, limit=limit)
275 283
276 ### 284 ###
277 # Reading data: Statuses 285 # Reading data: Statuses
@@ -282,7 +290,8 @@ class Mastodon:
282 290
283 Returns a toot dict. 291 Returns a toot dict.
284 """ 292 """
285 return self.__api_request('GET', '/api/v1/statuses/' + str(id)) 293 url = '/api/v1/statuses/{0}'.format(str(id))
294 return self.__api_request('GET', url)
286 295
287 def status_card(self, id): 296 def status_card(self, id):
288 """ 297 """
@@ -291,7 +300,8 @@ class Mastodon:
291 300
292 Returns a card dict. 301 Returns a card dict.
293 """ 302 """
294 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/card') 303 url = '/api/v1/statuses/{0}/card'.format(str(id))
304 return self.__api_request('GET', url)
295 305
296 def status_context(self, id): 306 def status_context(self, id):
297 """ 307 """
@@ -299,7 +309,8 @@ class Mastodon:
299 309
300 Returns a context dict. 310 Returns a context dict.
301 """ 311 """
302 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/context') 312 url = '/api/v1/statuses/{0}/context'.format(str(id))
313 return self.__api_request('GET', url)
303 314
304 def status_reblogged_by(self, id): 315 def status_reblogged_by(self, id):
305 """ 316 """
@@ -307,7 +318,8 @@ class Mastodon:
307 318
308 Returns a list of user dicts. 319 Returns a list of user dicts.
309 """ 320 """
310 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/reblogged_by') 321 url = '/api/v1/statuses/{0}/reblogged_by'.format(str(id))
322 return self.__api_request('GET', url)
311 323
312 def status_favourited_by(self, id): 324 def status_favourited_by(self, id):
313 """ 325 """
@@ -315,12 +327,13 @@ class Mastodon:
315 327
316 Returns a list of user dicts. 328 Returns a list of user dicts.
317 """ 329 """
318 return self.__api_request('GET', '/api/v1/statuses/' + str(id) + '/favourited_by') 330 url = '/api/v1/statuses/{0}/favourited_by'.format(str(id))
331 return self.__api_request('GET', url)
319 332
320 ### 333 ###
321 # Reading data: Notifications 334 # Reading data: Notifications
322 ### 335 ###
323 def notifications(self, id = None, max_id = None, since_id = None, limit = None): 336 def notifications(self, id=None, max_id=None, since_id=None, limit=None):
324 """ 337 """
325 Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated 338 Fetch notifications (mentions, favourites, reblogs, follows) for the authenticated
326 user. 339 user.
@@ -329,11 +342,12 @@ class Mastodon:
329 342
330 Returns a list of notification dicts. 343 Returns a list of notification dicts.
331 """ 344 """
332 if id == None: 345 if id is None:
333 params = self.__generate_params(locals(), ['id']) 346 params = self.__generate_params(locals(), ['id'])
334 return self.__api_request('GET', '/api/v1/notifications', params) 347 return self.__api_request('GET', '/api/v1/notifications', params)
335 else: 348 else:
336 return self.__api_request('GET', '/api/v1/notifications/' + str(id)) 349 url = '/api/v1/notifications/{0}'.format(str(id))
350 return self.__api_request('GET', url)
337 351
338 ### 352 ###
339 # Reading data: Accounts 353 # Reading data: Accounts
@@ -344,7 +358,8 @@ class Mastodon:
344 358
345 Returns a user dict. 359 Returns a user dict.
346 """ 360 """
347 return self.__api_request('GET', '/api/v1/accounts/' + str(id)) 361 url = '/api/v1/accounts/{0}'.format(str(id))
362 return self.__api_request('GET', url)
348 363
349 def account_verify_credentials(self): 364 def account_verify_credentials(self):
350 """ 365 """
@@ -354,32 +369,35 @@ class Mastodon:
354 """ 369 """
355 return self.__api_request('GET', '/api/v1/accounts/verify_credentials') 370 return self.__api_request('GET', '/api/v1/accounts/verify_credentials')
356 371
357 def account_statuses(self, id, max_id = None, since_id = None, limit = None): 372 def account_statuses(self, id, max_id=None, since_id=None, limit=None):
358 """ 373 """
359 Fetch statuses by user id. Same options as timeline are permitted. 374 Fetch statuses by user id. Same options as timeline are permitted.
360 375
361 Returns a list of toot dicts. 376 Returns a list of toot dicts.
362 """ 377 """
363 params = self.__generate_params(locals(), ['id']) 378 params = self.__generate_params(locals(), ['id'])
364 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/statuses', params) 379 url = '/api/v1/accounts/{0}/statuses'.format(str(id))
380 return self.__api_request('GET', url, params)
365 381
366 def account_following(self, id, max_id = None, since_id = None, limit = None): 382 def account_following(self, id, max_id=None, since_id=None, limit=None):
367 """ 383 """
368 Fetch users the given user is following. 384 Fetch users the given user is following.
369 385
370 Returns a list of user dicts. 386 Returns a list of user dicts.
371 """ 387 """
372 params = self.__generate_params(locals(), ['id']) 388 params = self.__generate_params(locals(), ['id'])
373 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/following', params) 389 url = '/api/v1/accounts/{0}/following'.format(str(id))
390 return self.__api_request('GET', url, params)
374 391
375 def account_followers(self, id, max_id = None, since_id = None, limit = None): 392 def account_followers(self, id, max_id=None, since_id=None, limit=None):
376 """ 393 """
377 Fetch users the given user is followed by. 394 Fetch users the given user is followed by.
378 395
379 Returns a list of user dicts. 396 Returns a list of user dicts.
380 """ 397 """
381 params = self.__generate_params(locals(), ['id']) 398 params = self.__generate_params(locals(), ['id'])
382 return self.__api_request('GET', '/api/v1/accounts/' + str(id) + '/followers', params) 399 url = '/api/v1/accounts/{0}/followers'.format(str(id))
400 return self.__api_request('GET', url, params)
383 401
384 def account_relationships(self, id): 402 def account_relationships(self, id):
385 """ 403 """
@@ -389,9 +407,10 @@ class Mastodon:
389 Returns a list of relationship dicts. 407 Returns a list of relationship dicts.
390 """ 408 """
391 params = self.__generate_params(locals()) 409 params = self.__generate_params(locals())
392 return self.__api_request('GET', '/api/v1/accounts/relationships', params) 410 return self.__api_request('GET', '/api/v1/accounts/relationships',
411 params)
393 412
394 def account_search(self, q, limit = None): 413 def account_search(self, q, limit=None):
395 """ 414 """
396 Fetch matching accounts. Will lookup an account remotely if the search term is 415 Fetch matching accounts. Will lookup an account remotely if the search term is
397 in the username@domain format and not yet in the database. 416 in the username@domain format and not yet in the database.
@@ -404,7 +423,7 @@ class Mastodon:
404 ### 423 ###
405 # Reading data: Searching 424 # Reading data: Searching
406 ### 425 ###
407 def search(self, q, resolve = False): 426 def search(self, q, resolve=False):
408 """ 427 """
409 Fetch matching hashtags, accounts and statuses. Will search federated 428 Fetch matching hashtags, accounts and statuses. Will search federated
410 instances if resolve is True. 429 instances if resolve is True.
@@ -417,7 +436,7 @@ class Mastodon:
417 ### 436 ###
418 # Reading data: Mutes and Blocks 437 # Reading data: Mutes and Blocks
419 ### 438 ###
420 def mutes(self, max_id = None, since_id = None, limit = None): 439 def mutes(self, max_id=None, since_id=None, limit=None):
421 """ 440 """
422 Fetch a list of users muted by the authenticated user. 441 Fetch a list of users muted by the authenticated user.
423 442
@@ -426,7 +445,7 @@ class Mastodon:
426 params = self.__generate_params(locals()) 445 params = self.__generate_params(locals())
427 return self.__api_request('GET', '/api/v1/mutes', params) 446 return self.__api_request('GET', '/api/v1/mutes', params)
428 447
429 def blocks(self, max_id = None, since_id = None, limit = None): 448 def blocks(self, max_id=None, since_id=None, limit=None):
430 """ 449 """
431 Fetch a list of users blocked by the authenticated user. 450 Fetch a list of users blocked by the authenticated user.
432 451
@@ -449,7 +468,7 @@ class Mastodon:
449 ### 468 ###
450 # Reading data: Favourites 469 # Reading data: Favourites
451 ### 470 ###
452 def favourites(self, max_id = None, since_id = None, limit = None): 471 def favourites(self, max_id=None, since_id=None, limit=None):
453 """ 472 """
454 Fetch the authenticated user's favourited statuses. 473 Fetch the authenticated user's favourited statuses.
455 474
@@ -461,7 +480,7 @@ class Mastodon:
461 ### 480 ###
462 # Reading data: Follow requests 481 # Reading data: Follow requests
463 ### 482 ###
464 def follow_requests(self, max_id = None, since_id = None, limit = None): 483 def follow_requests(self, max_id=None, since_id=None, limit=None):
465 """ 484 """
466 Fetch the authenticated user's incoming follow requests. 485 Fetch the authenticated user's incoming follow requests.
467 486
@@ -473,7 +492,7 @@ class Mastodon:
473 ### 492 ###
474 # Reading data: Domain blocks 493 # Reading data: Domain blocks
475 ### 494 ###
476 def domain_blocks(self, max_id = None, since_id = None, limit = None): 495 def domain_blocks(self, max_id=None, since_id=None, limit=None):
477 """ 496 """
478 Fetch the authenticated user's blocked domains. 497 Fetch the authenticated user's blocked domains.
479 498
@@ -481,11 +500,12 @@ class Mastodon:
481 """ 500 """
482 params = self.__generate_params(locals()) 501 params = self.__generate_params(locals())
483 return self.__api_request('GET', '/api/v1/domain_blocks', params) 502 return self.__api_request('GET', '/api/v1/domain_blocks', params)
484 503
485 ### 504 ###
486 # Writing data: Statuses 505 # Writing data: Statuses
487 ### 506 ###
488 def status_post(self, status, in_reply_to_id = None, media_ids = None, sensitive = False, visibility = '', spoiler_text = None): 507 def status_post(self, status, in_reply_to_id=None, media_ids=None,
508 sensitive=False, visibility='', spoiler_text=None):
489 """ 509 """
490 Post a status. Can optionally be in reply to another status and contain 510 Post a status. Can optionally be in reply to another status and contain
491 up to four pieces of media (Uploaded via media_post()). media_ids can 511 up to four pieces of media (Uploaded via media_post()). media_ids can
@@ -519,10 +539,10 @@ class Mastodon:
519 if params_initial['visibility'].lower() not in valid_visibilities: 539 if params_initial['visibility'].lower() not in valid_visibilities:
520 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities) 540 raise ValueError('Invalid visibility value! Acceptable values are %s' % valid_visibilities)
521 541
522 if params_initial['sensitive'] == False: 542 if params_initial['sensitive'] is False:
523 del[params_initial['sensitive']] 543 del [params_initial['sensitive']]
524 544
525 if media_ids != None: 545 if media_ids is not None:
526 try: 546 try:
527 media_ids_proper = [] 547 media_ids_proper = []
528 for media_id in media_ids: 548 for media_id in media_ids:
@@ -552,14 +572,16 @@ class Mastodon:
552 572
553 Returns an empty dict for good measure. 573 Returns an empty dict for good measure.
554 """ 574 """
555 return self.__api_request('DELETE', '/api/v1/statuses/' + str(id)) 575 url = '/api/v1/statuses/{0}'.format(str(id))
576 return self.__api_request('DELETE', url)
556 577
557 def status_reblog(self, id): 578 def status_reblog(self, id):
558 """Reblog a status. 579 """Reblog a status.
559 580
560 Returns a toot with with a new status that wraps around the reblogged one. 581 Returns a toot with with a new status that wraps around the reblogged one.
561 """ 582 """
562 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/reblog") 583 url = '/api/v1/statuses/{0}/reblog'.format(str(id))
584 return self.__api_request('POST', url)
563 585
564 def status_unreblog(self, id): 586 def status_unreblog(self, id):
565 """ 587 """
@@ -567,7 +589,8 @@ class Mastodon:
567 589
568 Returns a toot dict with the status that used to be reblogged. 590 Returns a toot dict with the status that used to be reblogged.
569 """ 591 """
570 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unreblog") 592 url = '/api/v1/statuses/{0}/unreblog'.format(str(id))
593 return self.__api_request('POST', url)
571 594
572 def status_favourite(self, id): 595 def status_favourite(self, id):
573 """ 596 """
@@ -575,7 +598,8 @@ class Mastodon:
575 598
576 Returns a toot dict with the favourited status. 599 Returns a toot dict with the favourited status.
577 """ 600 """
578 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/favourite") 601 url = '/api/v1/statuses/{0}/favourite'.format(str(id))
602 return self.__api_request('POST', url)
579 603
580 def status_unfavourite(self, id): 604 def status_unfavourite(self, id):
581 """ 605 """
@@ -583,7 +607,8 @@ class Mastodon:
583 607
584 Returns a toot dict with the un-favourited status. 608 Returns a toot dict with the un-favourited status.
585 """ 609 """
586 return self.__api_request('POST', '/api/v1/statuses/' + str(id) + "/unfavourite") 610 url = '/api/v1/statuses/{0}/unfavourite'.format(str(id))
611 return self.__api_request('POST', url)
587 612
588 ### 613 ###
589 # Writing data: Notifications 614 # Writing data: Notifications
@@ -603,7 +628,8 @@ class Mastodon:
603 628
604 Returns a relationship dict containing the updated relationship to the user. 629 Returns a relationship dict containing the updated relationship to the user.
605 """ 630 """
606 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/follow") 631 url = '/api/v1/accounts/{0}/follow'.format(str(id))
632 return self.__api_request('POST', url)
607 633
608 def follows(self, uri): 634 def follows(self, uri):
609 """ 635 """
@@ -620,7 +646,8 @@ class Mastodon:
620 646
621 Returns a relationship dict containing the updated relationship to the user. 647 Returns a relationship dict containing the updated relationship to the user.
622 """ 648 """
623 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unfollow") 649 url = '/api/v1/accounts/{0}/unfollow'.format(str(id))
650 return self.__api_request('POST', url)
624 651
625 def account_block(self, id): 652 def account_block(self, id):
626 """ 653 """
@@ -628,7 +655,8 @@ class Mastodon:
628 655
629 Returns a relationship dict containing the updated relationship to the user. 656 Returns a relationship dict containing the updated relationship to the user.
630 """ 657 """
631 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/block") 658 url = '/api/v1/accounts/{0}/block'.format(str(id))
659 return self.__api_request('POST', url)
632 660
633 def account_unblock(self, id): 661 def account_unblock(self, id):
634 """ 662 """
@@ -636,7 +664,8 @@ class Mastodon:
636 664
637 Returns a relationship dict containing the updated relationship to the user. 665 Returns a relationship dict containing the updated relationship to the user.
638 """ 666 """
639 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unblock") 667 url = '/api/v1/accounts/{0}/unblock'.format(str(id))
668 return self.__api_request('POST', url)
640 669
641 def account_mute(self, id): 670 def account_mute(self, id):
642 """ 671 """
@@ -644,7 +673,8 @@ class Mastodon:
644 673
645 Returns a relationship dict containing the updated relationship to the user. 674 Returns a relationship dict containing the updated relationship to the user.
646 """ 675 """
647 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/mute") 676 url = '/api/v1/accounts/{0}/mute'.format(str(id))
677 return self.__api_request('POST', url)
648 678
649 def account_unmute(self, id): 679 def account_unmute(self, id):
650 """ 680 """
@@ -652,9 +682,11 @@ class Mastodon:
652 682
653 Returns a relationship dict containing the updated relationship to the user. 683 Returns a relationship dict containing the updated relationship to the user.
654 """ 684 """
655 return self.__api_request('POST', '/api/v1/accounts/' + str(id) + "/unmute") 685 url = '/api/v1/accounts/{0}/unmute'.format(str(id))
686 return self.__api_request('POST', url)
656 687
657 def account_update_credentials(self, display_name = None, note = None, avatar = None, header = None): 688 def account_update_credentials(self, display_name=None, note=None,
689 avatar=None, header=None):
658 """ 690 """
659 Update the profile for the currently authenticated user. 691 Update the profile for the currently authenticated user.
660 692
@@ -689,7 +721,8 @@ class Mastodon:
689 721
690 Returns an empty dict. 722 Returns an empty dict.
691 """ 723 """
692 return self.__api_request('POST', '/api/v1/follow_requests/' + str(id) + "/authorize") 724 url = '/api/v1/follow_requests/{0}/authorize'.format(str(id))
725 return self.__api_request('POST', url)
693 726
694 def follow_request_reject(self, id): 727 def follow_request_reject(self, id):
695 """ 728 """
@@ -697,12 +730,13 @@ class Mastodon:
697 730
698 Returns an empty dict. 731 Returns an empty dict.
699 """ 732 """
700 return self.__api_request('POST', '/api/v1/follow_requests/' + str(id) + "/reject") 733 url = '/api/v1/follow_requests/{0}/reject'.format(str(id))
734 return self.__api_request('POST', url)
701 735
702 ### 736 ###
703 # Writing data: Media 737 # Writing data: Media
704 ### 738 ###
705 def media_post(self, media_file, mime_type = None): 739 def media_post(self, media_file, mime_type=None):
706 """ 740 """
707 Post an image. media_file can either be image data or 741 Post an image. media_file can either be image data or
708 a file name. If image data is passed directly, the mime 742 a file name. If image data is passed directly, the mime
@@ -715,45 +749,48 @@ class Mastodon:
715 Returns a media dict. This contains the id that can be used in 749 Returns a media dict. This contains the id that can be used in
716 status_post to attach the media file to a toot. 750 status_post to attach the media file to a toot.
717 """ 751 """
718 if mime_type == None and os.path.isfile(media_file): 752 if mime_type is None and os.path.isfile(media_file):
719 mime_type = mimetypes.guess_type(media_file)[0] 753 mime_type = mimetypes.guess_type(media_file)[0]
720 media_file = open(media_file, 'rb') 754 media_file = open(media_file, 'rb')
721 755
722 if mime_type == None: 756 if mime_type is None:
723 raise MastodonIllegalArgumentError('Could not determine mime type or data passed directly without mime type.') 757 raise MastodonIllegalArgumentError(
758 'Could not determine mime type or data passed directly without mime type.')
724 759
725 random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10)) 760 random_suffix = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(10))
726 file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(mime_type) 761 file_name = "mastodonpyupload_" + str(time.time()) + "_" + str(random_suffix) + mimetypes.guess_extension(
762 mime_type)
727 763
728 media_file_description = (file_name, media_file, mime_type) 764 media_file_description = (file_name, media_file, mime_type)
729 return self.__api_request('POST', '/api/v1/media', files = {'file': media_file_description}) 765 return self.__api_request('POST', '/api/v1/media',
766 files={'file': media_file_description})
730 767
731 ### 768 ###
732 # Writing data: Domain blocks 769 # Writing data: Domain blocks
733 ### 770 ###
734 def domain_block(self, domain = None): 771 def domain_block(self, domain=None):
735 """ 772 """
736 Add a block for all statuses originating from the specified domain for the logged-in user. 773 Add a block for all statuses originating from the specified domain for the logged-in user.
737 """ 774 """
738 params = self.__generate_params(locals()) 775 params = self.__generate_params(locals())
739 return self.__api_request('POST', '/api/v1/domain_blocks', params) 776 return self.__api_request('POST', '/api/v1/domain_blocks', params)
740 777
741 def domain_unblock(self, domain = None): 778 def domain_unblock(self, domain=None):
742 """ 779 """
743 Remove a domain block for the logged-in user. 780 Remove a domain block for the logged-in user.
744 """ 781 """
745 params = self.__generate_params(locals()) 782 params = self.__generate_params(locals())
746 return self.__api_request('DELETE', '/api/v1/domain_blocks', params) 783 return self.__api_request('DELETE', '/api/v1/domain_blocks', params)
747 784
748 ### 785 ###
749 # Pagination 786 # Pagination
750 ### 787 ###
751 def fetch_next(self, previous_page): 788 def fetch_next(self, previous_page):
752 """ 789 """
753 Fetches the next page of results of a paginated request. Pass in the 790 Fetches the next page of results of a paginated request. Pass in the
754 previous page in its entirety, or the pagination information dict 791 previous page in its entirety, or the pagination information dict
755 returned as a part of that pages last status ('_pagination_next'). 792 returned as a part of that pages last status ('_pagination_next').
756 793
757 Returns the next page or None if no further data is available. 794 Returns the next page or None if no further data is available.
758 """ 795 """
759 if isinstance(previous_page, list): 796 if isinstance(previous_page, list):
@@ -763,21 +800,21 @@ class Mastodon:
763 return None 800 return None
764 else: 801 else:
765 params = copy.deepcopy(previous_page) 802 params = copy.deepcopy(previous_page)
766 803
767 method = params['_pagination_method'] 804 method = params['_pagination_method']
768 del params['_pagination_method'] 805 del params['_pagination_method']
769 806
770 endpoint = params['_pagination_endpoint'] 807 endpoint = params['_pagination_endpoint']
771 del params['_pagination_endpoint'] 808 del params['_pagination_endpoint']
772 809
773 return self.__api_request(method, endpoint, params) 810 return self.__api_request(method, endpoint, params)
774 811
775 def fetch_previous(self, next_page): 812 def fetch_previous(self, next_page):
776 """ 813 """
777 Fetches the previous page of results of a paginated request. Pass in the 814 Fetches the previous page of results of a paginated request. Pass in the
778 previous page in its entirety, or the pagination information dict 815 previous page in its entirety, or the pagination information dict
779 returned as a part of that pages first status ('_pagination_prev'). 816 returned as a part of that pages first status ('_pagination_prev').
780 817
781 Returns the previous page or None if no further data is available. 818 Returns the previous page or None if no further data is available.
782 """ 819 """
783 if isinstance(next_page, list): 820 if isinstance(next_page, list):
@@ -787,34 +824,34 @@ class Mastodon:
787 return None 824 return None
788 else: 825 else:
789 params = copy.deepcopy(next_page) 826 params = copy.deepcopy(next_page)
790 827
791 method = params['_pagination_method'] 828 method = params['_pagination_method']
792 del params['_pagination_method'] 829 del params['_pagination_method']
793 830
794 endpoint = params['_pagination_endpoint'] 831 endpoint = params['_pagination_endpoint']
795 del params['_pagination_endpoint'] 832 del params['_pagination_endpoint']
796 833
797 return self.__api_request(method, endpoint, params) 834 return self.__api_request(method, endpoint, params)
798 835
799 def fetch_remaining(self, first_page): 836 def fetch_remaining(self, first_page):
800 """ 837 """
801 Fetches all the remaining pages of a paginated request starting from a 838 Fetches all the remaining pages of a paginated request starting from a
802 first page and returns the entire set of results (including the first page 839 first page and returns the entire set of results (including the first page
803 that was passed in) as a big list. 840 that was passed in) as a big list.
804 841
805 Be careful, as this might generate a lot of requests, depending on what you are 842 Be careful, as this might generate a lot of requests, depending on what you are
806 fetching, and might cause you to run into rate limits very quickly. 843 fetching, and might cause you to run into rate limits very quickly.
807 """ 844 """
808 first_page = copy.deepcopy(first_page) 845 first_page = copy.deepcopy(first_page)
809 846
810 all_pages = [] 847 all_pages = []
811 current_page = first_page 848 current_page = first_page
812 while current_page != None and len(current_page) > 0: 849 while current_page is not None and len(current_page) > 0:
813 all_pages.extend(current_page) 850 all_pages.extend(current_page)
814 current_page = self.fetch_next(current_page) 851 current_page = self.fetch_next(current_page)
815 852
816 return all_pages 853 return all_pages
817 854
818 ### 855 ###
819 # Streaming 856 # Streaming
820 ### 857 ###
@@ -858,7 +895,7 @@ class Mastodon:
858 incoming events. 895 incoming events.
859 """ 896 """
860 return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener) 897 return self.__stream("/api/v1/streaming/hashtag?tag={}".format(tag), listener)
861 898
862 ### 899 ###
863 # Internal helpers, dragons probably 900 # Internal helpers, dragons probably
864 ### 901 ###
@@ -870,22 +907,22 @@ class Mastodon:
870 Assumes UTC if timezone is not given. 907 Assumes UTC if timezone is not given.
871 """ 908 """
872 date_time_utc = None 909 date_time_utc = None
873 if date_time.tzinfo == None: 910 if date_time.tzinfo is None:
874 date_time_utc = date_time.replace(tzinfo = pytz.utc) 911 date_time_utc = date_time.replace(tzinfo=pytz.utc)
875 else: 912 else:
876 date_time_utc = date_time.astimezone(pytz.utc) 913 date_time_utc = date_time.astimezone(pytz.utc)
877 914
878 epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo = pytz.utc) 915 epoch_utc = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc)
879 916
880 return (date_time_utc - epoch_utc).total_seconds() 917 return (date_time_utc - epoch_utc).total_seconds()
881 918
882 def __api_request(self, method, endpoint, params = {}, files = {}, do_ratelimiting = True): 919 def __api_request(self, method, endpoint, params={}, files={}, do_ratelimiting=True):
883 """ 920 """
884 Internal API request helper. 921 Internal API request helper.
885 """ 922 """
886 response = None 923 response = None
887 headers = None 924 headers = None
888 925 remaining_wait = 0
889 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it 926 # "pace" mode ratelimiting: Assume constant rate of requests, sleep a little less long than it
890 # would take to not hit the rate limit at that request rate. 927 # would take to not hit the rate limit at that request rate.
891 if do_ratelimiting and self.ratelimit_method == "pace": 928 if do_ratelimiting and self.ratelimit_method == "pace":
@@ -906,10 +943,10 @@ class Mastodon:
906 time.sleep(to_next) 943 time.sleep(to_next)
907 944
908 # Generate request headers 945 # Generate request headers
909 if self.access_token != None: 946 if self.access_token is not None:
910 headers = {'Authorization': 'Bearer ' + self.access_token} 947 headers = {'Authorization': 'Bearer ' + self.access_token}
911 948
912 if self.debug_requests == True: 949 if self.debug_requests:
913 print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".') 950 print('Mastodon: Request to endpoint "' + endpoint + '" using method "' + method + '".')
914 print('Parameters: ' + str(params)) 951 print('Parameters: ' + str(params))
915 print('Headers: ' + str(headers)) 952 print('Headers: ' + str(headers))
@@ -923,24 +960,28 @@ class Mastodon:
923 response_object = None 960 response_object = None
924 try: 961 try:
925 if method == 'GET': 962 if method == 'GET':
926 response_object = requests.get(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 963 response_object = requests.get(self.api_base_url + endpoint, data=params, headers=headers,
964 files=files, timeout=self.request_timeout)
927 965
928 if method == 'POST': 966 if method == 'POST':
929 response_object = requests.post(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 967 response_object = requests.post(self.api_base_url + endpoint, data=params, headers=headers,
968 files=files, timeout=self.request_timeout)
930 969
931 if method == 'PATCH': 970 if method == 'PATCH':
932 response_object = requests.patch(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 971 response_object = requests.patch(self.api_base_url + endpoint, data=params, headers=headers,
972 files=files, timeout=self.request_timeout)
933 973
934 if method == 'DELETE': 974 if method == 'DELETE':
935 response_object = requests.delete(self.api_base_url + endpoint, data = params, headers = headers, files = files, timeout = self.request_timeout) 975 response_object = requests.delete(self.api_base_url + endpoint, data=params, headers=headers,
976 files=files, timeout=self.request_timeout)
936 except Exception as e: 977 except Exception as e:
937 raise MastodonNetworkError("Could not complete request: %s" % e) 978 raise MastodonNetworkError("Could not complete request: %s" % e)
938 979
939 if response_object == None: 980 if response_object is None:
940 raise MastodonIllegalArgumentError("Illegal request.") 981 raise MastodonIllegalArgumentError("Illegal request.")
941 982
942 # Handle response 983 # Handle response
943 if self.debug_requests == True: 984 if self.debug_requests:
944 print('Mastodon: Response received with code ' + str(response_object.status_code) + '.') 985 print('Mastodon: Response received with code ' + str(response_object.status_code) + '.')
945 print('response headers: ' + str(response_object.headers)) 986 print('response headers: ' + str(response_object.headers))
946 print('Response text content: ' + str(response_object.text)) 987 print('Response text content: ' + str(response_object.text))
@@ -954,20 +995,25 @@ class Mastodon:
954 try: 995 try:
955 response = response_object.json() 996 response = response_object.json()
956 except: 997 except:
957 raise MastodonAPIError("Could not parse response as JSON, response code was %s, bad json content was '%s'" % (response_object.status_code, response_object.content)) 998 raise MastodonAPIError(
999 "Could not parse response as JSON, response code was %s, bad json content was '%s'" % (
1000 response_object.status_code, response_object.content))
958 1001
959 # Parse link headers 1002 # Parse link headers
960 if isinstance(response, list) and 'Link' in response_object.headers and response_object.headers['Link'] != "": 1003 if isinstance(response, list) and \
961 tmp_urls = requests.utils.parse_header_links(response_object.headers['Link'].rstrip('>').replace('>,<', ',<')) 1004 'Link' in response_object.headers and \
1005 response_object.headers['Link'] != "":
1006 tmp_urls = requests.utils.parse_header_links(
1007 response_object.headers['Link'].rstrip('>').replace('>,<', ',<'))
962 for url in tmp_urls: 1008 for url in tmp_urls:
963 if not 'rel' in url: 1009 if not 'rel' in url:
964 continue 1010 continue
965 1011
966 if url['rel'] == 'next': 1012 if url['rel'] == 'next':
967 # Be paranoid and extract max_id specifically 1013 # Be paranoid and extract max_id specifically
968 next_url = url['url'] 1014 next_url = url['url']
969 matchgroups = re.search(r"max_id=([0-9]*)", next_url) 1015 matchgroups = re.search(r"max_id=([0-9]*)", next_url)
970 1016
971 if matchgroups: 1017 if matchgroups:
972 next_params = copy.deepcopy(params) 1018 next_params = copy.deepcopy(params)
973 next_params['_pagination_method'] = method 1019 next_params['_pagination_method'] = method
@@ -976,12 +1022,12 @@ class Mastodon:
976 if "since_id" in next_params: 1022 if "since_id" in next_params:
977 del next_params['since_id'] 1023 del next_params['since_id']
978 response[-1]['_pagination_next'] = next_params 1024 response[-1]['_pagination_next'] = next_params
979 1025
980 if url['rel'] == 'prev': 1026 if url['rel'] == 'prev':
981 # Be paranoid and extract since_id specifically 1027 # Be paranoid and extract since_id specifically
982 prev_url = url['url'] 1028 prev_url = url['url']
983 matchgroups = re.search(r"since_id=([0-9]*)", prev_url) 1029 matchgroups = re.search(r"since_id=([0-9]*)", prev_url)
984 1030
985 if matchgroups: 1031 if matchgroups:
986 prev_params = copy.deepcopy(params) 1032 prev_params = copy.deepcopy(params)
987 prev_params['_pagination_method'] = method 1033 prev_params['_pagination_method'] = method
@@ -990,7 +1036,7 @@ class Mastodon:
990 if "max_id" in prev_params: 1036 if "max_id" in prev_params:
991 del prev_params['max_id'] 1037 del prev_params['max_id']
992 response[0]['_pagination_prev'] = prev_params 1038 response[0]['_pagination_prev'] = prev_params
993 1039
994 # Handle rate limiting 1040 # Handle rate limiting
995 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting: 1041 if 'X-RateLimit-Remaining' in response_object.headers and do_ratelimiting:
996 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining']) 1042 self.ratelimit_remaining = int(response_object.headers['X-RateLimit-Remaining'])
@@ -1024,21 +1070,20 @@ class Mastodon:
1024 1070
1025 return response 1071 return response
1026 1072
1027 def __stream(self, endpoint, listener, params = {}): 1073 def __stream(self, endpoint, listener, params={}):
1028 """ 1074 """
1029 Internal streaming API helper. 1075 Internal streaming API helper.
1030 """ 1076 """
1031 1077
1032 headers = {} 1078 headers = {}
1033 if self.access_token != None: 1079 if self.access_token is not None:
1034 headers = {'Authorization': 'Bearer ' + self.access_token} 1080 headers = {'Authorization': 'Bearer ' + self.access_token}
1035 1081
1036 url = self.api_base_url + endpoint 1082 url = self.api_base_url + endpoint
1037 with closing(requests.get(url, headers = headers, data = params, stream = True)) as r: 1083 with closing(requests.get(url, headers=headers, data=params, stream=True)) as r:
1038 listener.handle_stream(r.iter_lines()) 1084 listener.handle_stream(r.iter_lines())
1039 1085
1040 1086 def __generate_params(self, params, exclude=[]):
1041 def __generate_params(self, params, exclude = []):
1042 """ 1087 """
1043 Internal named-parameters-to-dict helper. 1088 Internal named-parameters-to-dict helper.
1044 1089
@@ -1052,7 +1097,7 @@ class Mastodon:
1052 del params['self'] 1097 del params['self']
1053 param_keys = list(params.keys()) 1098 param_keys = list(params.keys())
1054 for key in param_keys: 1099 for key in param_keys:
1055 if params[key] == None or key in exclude: 1100 if params[key] is None or key in exclude:
1056 del params[key] 1101 del params[key]
1057 1102
1058 param_keys = list(params.keys()) 1103 param_keys = list(params.keys())
@@ -1063,28 +1108,24 @@ class Mastodon:
1063 1108
1064 return params 1109 return params
1065 1110
1066
1067 def __get_token_expired(self): 1111 def __get_token_expired(self):
1068 """Internal helper for oauth code""" 1112 """Internal helper for oauth code"""
1069 if self._token_expired < datetime.datetime.now(): 1113 return self._token_expired < datetime.datetime.now()
1070 return True
1071 else:
1072 return False
1073 1114
1074 def __set_token_expired(self, value): 1115 def __set_token_expired(self, value):
1075 """Internal helper for oauth code""" 1116 """Internal helper for oauth code"""
1076 self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value) 1117 self._token_expired = datetime.datetime.now() + datetime.timedelta(seconds=value)
1077 return 1118 return
1078 1119
1079 def __get_refresh_token(self): 1120 def __get_refresh_token(self):
1080 """Internal helper for oauth code""" 1121 """Internal helper for oauth code"""
1081 return self._refresh_token 1122 return self._refresh_token
1082 1123
1083 def __set_refresh_token(self, value): 1124 def __set_refresh_token(self, value):
1084 """Internal helper for oauth code""" 1125 """Internal helper for oauth code"""
1085 self._refresh_token = value 1126 self._refresh_token = value
1086 return 1127 return
1087 1128
1088 @staticmethod 1129 @staticmethod
1089 def __protocolize(base_url): 1130 def __protocolize(base_url):
1090 """Internal add-protocol-to-url helper""" 1131 """Internal add-protocol-to-url helper"""
@@ -1095,21 +1136,25 @@ class Mastodon:
1095 base_url = base_url.rstrip("/") 1136 base_url = base_url.rstrip("/")
1096 return base_url 1137 return base_url
1097 1138
1139
1098## 1140##
1099# Exceptions 1141# Exceptions
1100## 1142##
1101class MastodonIllegalArgumentError(ValueError): 1143class MastodonIllegalArgumentError(ValueError):
1102 pass 1144 pass
1103 1145
1146
1104class MastodonFileNotFoundError(IOError): 1147class MastodonFileNotFoundError(IOError):
1105 pass 1148 pass
1106 1149
1150
1107class MastodonNetworkError(IOError): 1151class MastodonNetworkError(IOError):
1108 pass 1152 pass
1109 1153
1154
1110class MastodonAPIError(Exception): 1155class MastodonAPIError(Exception):
1111 pass 1156 pass
1112 1157
1158
1113class MastodonRatelimitError(Exception): 1159class MastodonRatelimitError(Exception):
1114 pass 1160 pass
1115
diff --git a/mastodon/streaming.py b/mastodon/streaming.py
index 3212848..290ed44 100644
--- a/mastodon/streaming.py
+++ b/mastodon/streaming.py
@@ -1,7 +1,7 @@
1''' 1"""
2Handlers for the Streaming API: 2Handlers for the Streaming API:
3https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md 3https://github.com/tootsuite/mastodon/blob/master/docs/Using-the-API/Streaming-API.md
4''' 4"""
5 5
6import json 6import json
7import logging 7import logging
@@ -12,43 +12,43 @@ log = logging.getLogger(__name__)
12 12
13 13
14class MalformedEventError(Exception): 14class MalformedEventError(Exception):
15 '''Raised when the server-sent event stream is malformed.''' 15 """Raised when the server-sent event stream is malformed."""
16 pass 16 pass
17 17
18 18
19class StreamListener(object): 19class StreamListener(object):
20 '''Callbacks for the streaming API. Create a subclass, override the on_xxx 20 """Callbacks for the streaming API. Create a subclass, override the on_xxx
21 methods for the kinds of events you're interested in, then pass an instance 21 methods for the kinds of events you're interested in, then pass an instance
22 of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or 22 of your subclass to Mastodon.user_stream(), Mastodon.public_stream(), or
23 Mastodon.hashtag_stream().''' 23 Mastodon.hashtag_stream()."""
24 24
25 def on_update(self, status): 25 def on_update(self, status):
26 '''A new status has appeared! 'status' is the parsed JSON dictionary 26 """A new status has appeared! 'status' is the parsed JSON dictionary
27 describing the status.''' 27describing the status."""
28 pass 28 pass
29 29
30 def on_notification(self, notification): 30 def on_notification(self, notification):
31 '''A new notification. 'notification' is the parsed JSON dictionary 31 """A new notification. 'notification' is the parsed JSON dictionary
32 describing the notification.''' 32 describing the notification."""
33 pass 33 pass
34 34
35 def on_delete(self, status_id): 35 def on_delete(self, status_id):
36 '''A status has been deleted. status_id is the status' integer ID.''' 36 """A status has been deleted. status_id is the status' integer ID."""
37 pass 37 pass
38 38
39 def handle_heartbeat(self): 39 def handle_heartbeat(self):
40 '''The server has sent us a keep-alive message. This callback may be 40 """The server has sent us a keep-alive message. This callback may be
41 useful to carry out periodic housekeeping tasks, or just to confirm 41 useful to carry out periodic housekeeping tasks, or just to confirm
42 that the connection is still open.''' 42 that the connection is still open."""
43 43
44 def handle_stream(self, lines): 44 def handle_stream(self, lines):
45 ''' 45 """
46 Handles a stream of events from the Mastodon server. When each event 46 Handles a stream of events from the Mastodon server. When each event
47 is received, the corresponding .on_[name]() method is called. 47 is received, the corresponding .on_[name]() method is called.
48 48
49 lines: an iterable of lines of bytes sent by the Mastodon server, as 49 lines: an iterable of lines of bytes sent by the Mastodon server, as
50 returned by requests.Response.iter_lines(). 50 returned by requests.Response.iter_lines().
51 ''' 51 """
52 event = {} 52 event = {}
53 for raw_line in lines: 53 for raw_line in lines:
54 try: 54 try:
@@ -104,4 +104,3 @@ class StreamListener(object):
104 else: 104 else:
105 # TODO: allow handlers to return/raise to stop streaming cleanly 105 # TODO: allow handlers to return/raise to stop streaming cleanly
106 handler(payload) 106 handler(payload)
107
diff --git a/setup.py b/setup.py
index 20c1bf1..0ebf236 100644
--- a/setup.py
+++ b/setup.py
@@ -1,4 +1,4 @@
1from setuptools import setup, find_packages 1from setuptools import setup
2 2
3setup(name='Mastodon.py', 3setup(name='Mastodon.py',
4 version='1.0.8', 4 version='1.0.8',
@@ -6,7 +6,7 @@ setup(name='Mastodon.py',
6 packages=['mastodon'], 6 packages=['mastodon'],
7 setup_requires=['pytest-runner'], 7 setup_requires=['pytest-runner'],
8 tests_require=['pytest'], 8 tests_require=['pytest'],
9 install_requires=['requests', 'python-dateutil', 'six'], 9 install_requires=['requests', 'python-dateutil', 'six', 'pytz'],
10 url='https://github.com/halcy/Mastodon.py', 10 url='https://github.com/halcy/Mastodon.py',
11 author='Lorenz Diener', 11 author='Lorenz Diener',
12 author_email='[email protected]', 12 author_email='[email protected]',
@@ -19,5 +19,4 @@ setup(name='Mastodon.py',
19 'License :: OSI Approved :: MIT License', 19 'License :: OSI Approved :: MIT License',
20 'Programming Language :: Python :: 2', 20 'Programming Language :: Python :: 2',
21 'Programming Language :: Python :: 3', 21 'Programming Language :: Python :: 3',
22 ] 22 ])
23)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/test_streaming.py b/tests/test_streaming.py
index c79a8e9..1f8d75f 100644
--- a/tests/test_streaming.py
+++ b/tests/test_streaming.py
@@ -24,9 +24,10 @@ class Listener(StreamListener):
24 self.heartbeats += 1 24 self.heartbeats += 1
25 25
26 def handle_stream_(self, lines): 26 def handle_stream_(self, lines):
27 '''Test helper to avoid littering all tests with six.b().''' 27 """Test helper to avoid littering all tests with six.b()."""
28 return self.handle_stream(map(six.b, lines)) 28 return self.handle_stream(map(six.b, lines))
29 29
30
30def test_heartbeat(): 31def test_heartbeat():
31 listener = Listener() 32 listener = Listener()
32 listener.handle_stream_([':one', ':two']) 33 listener.handle_stream_([':one', ':two'])
@@ -85,7 +86,7 @@ def test_many(events):
85 86
86 87
87def test_unknown_event(): 88def test_unknown_event():
88 '''Be tolerant of new event types''' 89 """Be tolerant of new event types"""
89 listener = Listener() 90 listener = Listener()
90 listener.handle_stream_([ 91 listener.handle_stream_([
91 'event: blahblah', 92 'event: blahblah',
@@ -137,11 +138,11 @@ def test_sse_order_doesnt_matter():
137 138
138 139
139def test_extra_keys_ignored(): 140def test_extra_keys_ignored():
140 ''' 141 """
141 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format 142 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format
142 defines 'id' and 'retry' keys which the Mastodon streaming API doesn't use, 143 defines 'id' and 'retry' keys which the Mastodon streaming API doesn't use,
143 and alleges that "All other field names are ignored". 144 and alleges that "All other field names are ignored".
144 ''' 145 """
145 listener = Listener() 146 listener = Listener()
146 listener.handle_stream_([ 147 listener.handle_stream_([
147 'event: update', 148 'event: update',
@@ -155,7 +156,7 @@ def test_extra_keys_ignored():
155 156
156 157
157def test_valid_utf8(): 158def test_valid_utf8():
158 '''Snowman Cat Face With Tears Of Joy''' 159 """Snowman Cat Face With Tears Of Joy"""
159 listener = Listener() 160 listener = Listener()
160 listener.handle_stream_([ 161 listener.handle_stream_([
161 'event: update', 162 'event: update',
@@ -166,7 +167,7 @@ def test_valid_utf8():
166 167
167 168
168def test_invalid_utf8(): 169def test_invalid_utf8():
169 '''Cat Face With Tears O''' 170 """Cat Face With Tears O"""
170 listener = Listener() 171 listener = Listener()
171 with pytest.raises(MalformedEventError): 172 with pytest.raises(MalformedEventError):
172 listener.handle_stream_([ 173 listener.handle_stream_([
@@ -177,13 +178,13 @@ def test_invalid_utf8():
177 178
178 179
179def test_multiline_payload(): 180def test_multiline_payload():
180 ''' 181 """
181 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Data-only_messages 182 https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Data-only_messages
182 says that newlines in the 'data' field can be encoded by sending the field 183 says that newlines in the 'data' field can be encoded by sending the field
183 twice! This would be really pathological for Mastodon because the payload 184 twice! This would be really pathological for Mastodon because the payload
184 is JSON, but technically literal newlines are permissible (outside strings) 185 is JSON, but technically literal newlines are permissible (outside strings)
185 so let's handle this case. 186 so let's handle this case.
186 ''' 187 """
187 listener = Listener() 188 listener = Listener()
188 listener.handle_stream_([ 189 listener.handle_stream_([
189 'event: update', 190 'event: update',
Powered by cgit v1.2.3 (git 2.41.0)