pydoc will now correctly read doctsing for diaspy/streams.py,
[diaspy.git] / diaspy / streams.py
1 """Docstrings for this module are taken from:
2 https://gist.github.com/MrZYX/01c93096c30dc44caf71
3
4 Documentation for D* JSON API taken from:
5 http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
6 """
7
8
9 import json
10 import time
11 from diaspy.models import Post, Aspect
12 from diaspy import errors
13
14
15 class Generic():
16 """Object representing generic stream.
17 """
18 _location = 'stream.json'
19
20 def __init__(self, connection, location=''):
21 """
22 :param connection: Connection() object
23 :type connection: diaspy.connection.Connection
24 :param location: location of json (optional)
25 :type location: str
26 """
27 self._connection = connection
28 if location: self._location = location
29 self._stream = []
30 # since epoch
31 self.max_time = int(time.mktime(time.gmtime()))
32 self.fill()
33
34 def __contains__(self, post):
35 """Returns True if stream contains given post.
36 """
37 return post in self._stream
38
39 def __iter__(self):
40 """Provides iterable interface for stream.
41 """
42 return iter(self._stream)
43
44 def __getitem__(self, n):
45 """Returns n-th item in Stream.
46 """
47 return self._stream[n]
48
49 def __len__(self):
50 """Returns length of the Stream.
51 """
52 return len(self._stream)
53
54 def _obtain(self, max_time=0):
55 """Obtains stream from pod.
56 """
57 params = {}
58 if max_time: params['max_time'] = max_time
59 request = self._connection.get(self._location, params=params)
60 if request.status_code != 200:
61 raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
62 return [Post(self._connection, post['id']) for post in request.json()]
63
64 def _expand(self, new_stream):
65 """Appends older posts to stream.
66 """
67 ids = [post.post_id for post in self._stream]
68 stream = self._stream
69 for post in new_stream:
70 if post.post_id not in ids:
71 stream.append(post)
72 ids.append(post.post_id)
73 self._stream = stream
74
75 def _update(self, new_stream):
76 """Updates stream with new posts.
77 """
78 ids = [post.id for post in self._stream]
79
80 stream = self._stream
81 for i in range(len(new_stream)):
82 if new_stream[-i].id not in ids:
83 stream = [new_stream[-i]] + stream
84 ids.append(new_stream[-i].id)
85 self._stream = stream
86
87 def clear(self):
88 """Removes all posts from stream.
89 """
90 self._stream = []
91
92 def purge(self):
93 """Removes all unexistent posts from stream.
94 """
95 stream = []
96 for post in self._stream:
97 deleted = False
98 try:
99 post.update()
100 stream.append(post)
101 except Exception:
102 deleted = True
103 finally:
104 if not deleted: stream.append(post)
105 self._stream = stream
106
107 def update(self):
108 """Updates stream.
109 """
110 self._update(self._obtain())
111
112 def fill(self):
113 """Fills the stream with posts.
114 """
115 self._stream = self._obtain()
116
117 def more(self, max_time=0):
118 """Tries to download more (older ones) Posts from Stream.
119
120 :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
121 :type max_time: int
122 """
123 if not max_time: max_time = self.max_time - 3000000
124 self.max_time = max_time
125 new_stream = self._obtain(max_time=max_time)
126 self._expand(new_stream)
127
128
129 class Outer(Generic):
130 """Object used by diaspy.models.User to represent
131 stream of other user.
132 """
133 def _obtain(self):
134 """Obtains stream of other user.
135 """
136 request = self._connection.get(self._location)
137 if request.status_code != 200:
138 raise error.StreamError('wrong status code: {0}'.format(request.status_code))
139 return [Post(self._connection, post['id']) for post in request.json()]
140
141
142 class Stream(Generic):
143 """The main stream containing the combined posts of the
144 followed users and tags and the community spotlights posts
145 if the user enabled those.
146 """
147 location = 'stream.json'
148
149 def post(self, text='', aspect_ids='public', photos=None, photo=''):
150 """This function sends a post to an aspect.
151 If both `photo` and `photos` are specified `photos` takes precedence.
152
153 :param text: Text to post.
154 :type text: str
155 :param aspect_ids: Aspect ids to send post to.
156 :type aspect_ids: str
157 :param photo: filename of photo to post
158 :type photo: str
159 :param photos: id of photo to post (obtained from _photoupload())
160 :type photos: int
161
162 :returns: diaspy.models.Post -- the Post which has been created
163 """
164 data = {}
165 data['aspect_ids'] = aspect_ids
166 data['status_message'] = {'text': text}
167 if photo: data['photos'] = self._photoupload(photo)
168 if photos: data['photos'] = photos
169
170 request = self._connection.post('status_messages',
171 data=json.dumps(data),
172 headers={'content-type': 'application/json',
173 'accept': 'application/json',
174 'x-csrf-token': repr(self._connection)})
175 if request.status_code != 201:
176 raise Exception('{0}: Post could not be posted.'.format(request.status_code))
177
178 post = Post(self._connection, request.json()['id'])
179 return post
180
181 def _photoupload(self, filename):
182 """Uploads picture to the pod.
183
184 :param filename: path to picture file
185 :type filename: str
186
187 :returns: id of the photo being uploaded
188 """
189 data = open(filename, 'rb')
190 image = data.read()
191 data.close()
192
193 params = {}
194 params['photo[pending]'] = 'true'
195 params['set_profile_image'] = ''
196 params['qqfile'] = filename
197 aspects = self._connection.getUserInfo()['aspects']
198 for i, aspect in enumerate(aspects):
199 params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
200
201 headers = {'content-type': 'application/octet-stream',
202 'x-csrf-token': repr(self._connection),
203 'x-file-name': filename}
204
205 request = self._connection.post('photos', data=image, params=params, headers=headers)
206 if request.status_code != 200:
207 raise Exception('wrong error code: {0}'.format(request.status_code))
208 return request.json()['data']['photo']['id']
209
210
211 class Activity(Stream):
212 """Stream representing user's activity.
213 """
214 _location = 'activity.json'
215
216 def _delid(self, id):
217 """Deletes post with given id.
218 """
219 post = None
220 for p in self._stream:
221 if p['id'] == id:
222 post = p
223 break
224 if post is not None: post.delete()
225
226 def delete(self, post):
227 """Deletes post from users activity.
228 `post` can be either post id or Post()
229 object which will be identified and deleted.
230 After deleting post the stream will be filled.
231
232 :param post: post identifier
233 :type post: str, diaspy.models.Post
234 """
235 if type(post) == str: self._delid(post)
236 elif type(post) == Post: post.delete()
237 else: raise TypeError('this method accepts str or Post types: {0} given')
238 self.fill()
239
240
241 class Aspects(Generic):
242 """This stream contains the posts filtered by
243 the specified aspect IDs. You can choose the aspect IDs with
244 the parameter `aspect_ids` which value should be
245 a comma seperated list of aspect IDs.
246 If the parameter is ommitted all aspects are assumed.
247 An example call would be `aspects.json?aspect_ids=23,5,42`
248 """
249 _location = 'aspects.json'
250
251 def getAspectID(self, aspect_name):
252 """Returns id of an aspect of given name.
253 Returns -1 if aspect is not found.
254
255 :param aspect_name: aspect name (must be spelled exactly as when created)
256 :type aspect_name: str
257 :returns: int
258 """
259 id = -1
260 aspects = self._connection.getUserInfo()['aspects']
261 for aspect in aspects:
262 if aspect['name'] == aspect_name: id = aspect['id']
263 return id
264
265 def filterByIDs(self, ids):
266 self._location += '?{0}'.format(','.join(ids))
267 self.fill()
268
269 def add(self, aspect_name, visible=0):
270 """This function adds a new aspect.
271 Status code 422 is accepted because it is returned by D* when
272 you try to add aspect already present on your aspect list.
273
274 :returns: Aspect() object of just created aspect
275 """
276 data = {'authenticity_token': self._connection.get_token(),
277 'aspect[name]': aspect_name,
278 'aspect[contacts_visible]': visible}
279
280 request = self._connection.post('aspects', data=data)
281 if request.status_code not in [200, 422]:
282 raise Exception('wrong status code: {0}'.format(request.status_code))
283
284 id = self.getAspectID(aspect_name)
285 return Aspect(self._connection, id)
286
287 def remove(self, aspect_id=-1, name=''):
288 """This method removes an aspect.
289 You can give it either id or name of the aspect.
290 When both are specified, id takes precedence over name.
291
292 Status code 500 is accepted because although the D* will
293 go nuts it will remove the aspect anyway.
294
295 :param aspect_id: id fo aspect to remove
296 :type aspect_id: int
297 :param name: name of aspect to remove
298 :type name: str
299 """
300 if aspect_id == -1 and name: aspect_id = self.getAspectID(name)
301 data = {'_method': 'delete',
302 'authenticity_token': self._connection.get_token()}
303 request = self._connection.post('aspects/{0}'.format(aspect_id), data=data)
304 if request.status_code not in [200, 302, 500]:
305 raise Exception('wrong status code: {0}: cannot remove aspect'.format(request.status_code))
306
307
308 class Commented(Generic):
309 """This stream contains all posts
310 the user has made a comment on.
311 """
312 _location = 'commented.json'
313
314
315 class Liked(Generic):
316 """This stream contains all posts the user liked.
317 """
318 _location = 'liked.json'
319
320
321 class Mentions(Generic):
322 """This stream contains all posts
323 the user is mentioned in.
324 """
325 _location = 'mentions.json'
326
327
328 class FollowedTags(Generic):
329 """This stream contains all posts
330 containing tags the user is following.
331 """
332 _location = 'followed_tags.json'
333
334 def remove(self, tag_id):
335 """Stop following a tag.
336
337 :param tag_id: tag id
338 :type tag_id: int
339 """
340 data = {'authenticity_token': self._connection.get_token()}
341 request = self._connection.delete('tag_followings/{0}'.format(tag_id), data=data)
342 if request.status_code != 404:
343 raise Exception('wrong status code: {0}'.format(request.status_code))
344
345 def add(self, tag_name):
346 """Follow new tag.
347 Error code 403 is accepted because pods respod with it when request
348 is sent to follow a tag that a user already follows.
349
350 :param tag_name: tag name
351 :type tag_name: str
352 :returns: int (response code)
353 """
354 data = {'name': tag_name,
355 'authenticity_token': self._connection.get_token(),
356 }
357 headers = {'content-type': 'application/json',
358 'x-csrf-token': self._connection.get_token(),
359 'accept': 'application/json'
360 }
361
362 request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
363
364 if request.status_code not in [201, 403]:
365 raise Exception('wrong error code: {0}'.format(request.status_code))
366 return request.status_code
367
368
369 class Tag(Generic):
370 """This stream contains all posts containing a tag.
371 """
372 def __init__(self, connection, tag):
373 """
374 :param connection: Connection() object
375 :type connection: diaspy.connection.Connection
376 :param tag: tag name
377 :type tag: str
378 """
379 self._connection = connection
380 self._location = 'tags/{0}.json'.format(tag)
381 self.fill()