-import json
-import re
-import time
-from diaspy.models import Post
-
"""Docstrings for this module are taken from:
https://gist.github.com/MrZYX/01c93096c30dc44caf71
"""
-class Generic:
- """Object representing generic stream. Used in Tag(),
- Stream(), Activity() etc.
+import json
+import time
+from diaspy.models import Post, Aspect
+from diaspy import errors
+
+
+class Generic():
+ """Object representing generic stream.
"""
_location = 'stream.json'
- _stream = []
- # since epoch
- max_time = int(time.mktime(time.gmtime()))
def __init__(self, connection, location=''):
"""
"""
self._connection = connection
if location: self._location = location
+ self._stream = []
+ # since epoch
+ self.max_time = int(time.mktime(time.gmtime()))
self.fill()
def __contains__(self, post):
"""Returns True if stream contains given post.
"""
- if type(post) is not Post:
- raise TypeError('stream can contain only posts: checked for {0}'.format(type(post)))
return post in self._stream
def __iter__(self):
"""
return len(self._stream)
- def _obtain(self):
+ def _obtain(self, max_time=0):
"""Obtains stream from pod.
"""
- request = self._connection.get(self._location)
+ params = {}
+ if max_time:
+ params['max_time'] = max_time
+ params['_'] = int(time.time() * 1000)
+ request = self._connection.get(self._location, params=params)
if request.status_code != 200:
- raise Exception('wrong status code: {0}'.format(request.status_code))
- return [Post(str(post['id']), self._connection) for post in request.json()]
+ raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ return [Post(self._connection, post['id']) for post in request.json()]
+
+ def _expand(self, new_stream):
+ """Appends older posts to stream.
+ """
+ ids = [post.id for post in self._stream]
+ stream = self._stream
+ for post in new_stream:
+ if post.id not in ids:
+ stream.append(post)
+ ids.append(post.id)
+ self._stream = stream
+
+ def _update(self, new_stream):
+ """Updates stream with new posts.
+ """
+ ids = [post.id for post in self._stream]
+
+ stream = self._stream
+ for i in range(len(new_stream)):
+ if new_stream[-i].id not in ids:
+ stream = [new_stream[-i]] + stream
+ ids.append(new_stream[-i].id)
+ self._stream = stream
def clear(self):
"""Removes all posts from stream.
for post in self._stream:
deleted = False
try:
- post.get_data()
+ post.update()
stream.append(post)
except Exception:
deleted = True
def update(self):
"""Updates stream.
"""
- new_stream = self._obtain()
- ids = [post.post_id for post in self._stream]
-
- stream = self._stream
- for i in range(len(new_stream)):
- if new_stream[-i].post_id not in ids:
- stream = [new_stream[-i]] + stream
- ids.append(new_stream[-i].post_id)
-
- self._stream = stream
+ self._update(self._obtain())
def fill(self):
"""Fills the stream with posts.
"""
self._stream = self._obtain()
- def more(self):
+ def more(self, max_time=0, backtime=84600):
"""Tries to download more (older ones) Posts from Stream.
+
+ :param backtime: how many seconds substract each time (defaults to one day)
+ :type backtime: int
+ :param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
+ :type max_time: int
"""
- self.max_time -= 3000000
- params = {'max_time':self.max_time}
- request = self._connection.get('{0}', params=params)
- if request.status_code != 200:
- raise Exception('wrong status code: {0}'.format(request.status_code))
+ if not max_time: max_time = self.max_time - backtime
+ self.max_time = max_time
+ new_stream = self._obtain(max_time=max_time)
+ self._expand(new_stream)
+
+ def full(self, backtime=84600, retry=42, callback=None):
+ """Fetches full stream - containing all posts.
+ WARNING: this is a **VERY** long running function.
+ Use callback parameter to access information about the stream during its
+ run.
+
+ Default backtime is one day. But sometimes user might not have any activity for longer
+ period (on the beginning I posted once a month or so).
+ The role of retry is to hadle such situations by trying to go further back in time.
+ If a post is found the counter is restored.
+
+ :param backtime: how many seconds to substract each time
+ :type backtime: int
+ :param retry: how many times the functin should look deeper than your last post
+ :type retry: int
+ :param callback: callable taking diaspy.streams.Generic as an argument
+ :returns: integer, lenght of the stream
+ """
+ oldstream = self.copy()
+ self.more()
+ while len(oldstream) < len(self):
+ oldstream = self.copy()
+ if callback is not None: callback(self)
+ self.more(backtime=backtime)
+ if len(oldstream) < len(self): continue
+ # but if no posts were found start retrying...
+ print('retrying... {0}'.format(retry))
+ n = retry
+ while n > 0:
+ print('\t', n, self.max_time)
+ # try to get even more posts...
+ self.more(backtime=backtime)
+ print('\t', len(oldstream), len(self))
+ # check if it was a success...
+ if len(oldstream) < len(self):
+ # and if so restore normal order of execution by
+ # going one loop higher
+ break
+ oldstream = self.copy()
+ # if it was not a success substract one day, keep calm and
+ # try going further rback in time...
+ n -= 1
+ #if len(oldstream) == len(self): break
+ return len(self)
+
+ def copy(self):
+ """Returns copy (list of posts) of current stream.
+ """
+ return [p for p in self._stream]
+
+ def json(self, comments=False):
+ """Returns JSON encoded string containing stream's data.
+
+ :param comments: to include comments or not to include 'em, that is the question this param holds answer to
+ :type comments: bool
+ """
+ stream = [post for post in self._stream]
+ if comments:
+ for i, post in enumerate(stream):
+ post._fetchcomments()
+ comments = [c.data for c in post.comments]
+ post['interactions']['comments'] = comments
+ stream[i] = post
+ stream = [post.data for post in stream]
+ return json.dumps(stream)
class Outer(Generic):
"""Object used by diaspy.models.User to represent
stream of other user.
"""
- def _obtain(self):
- """Obtains stream of other user.
+ def _obtain(self, max_time=0):
+ """Obtains stream from pod.
"""
- request = self._connection.get(self._location)
+ params = {}
+ if max_time: params['max_time'] = max_time
+ request = self._connection.get(self._location, params=params)
if request.status_code != 200:
- raise Exception('wrong status code: {0}'.format(request.status_code))
- return [Post(str(post['id']), self._connection) for post in request.json()]
+ raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ return [Post(self._connection, post['id']) for post in request.json()]
class Stream(Generic):
data=json.dumps(data),
headers={'content-type': 'application/json',
'accept': 'application/json',
- 'x-csrf-token': self._connection.get_token()})
+ 'x-csrf-token': repr(self._connection)})
if request.status_code != 201:
raise Exception('{0}: Post could not be posted.'.format(request.status_code))
- post = Post(str(request.json()['id']), self._connection)
+ post = Post(self._connection, request.json()['id'])
return post
def _photoupload(self, filename):
params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
headers = {'content-type': 'application/octet-stream',
- 'x-csrf-token': self._connection.get_token(),
+ 'x-csrf-token': repr(self._connection),
'x-file-name': filename}
request = self._connection.post('photos', data=image, params=params, headers=headers)
if request.status_code != 200:
- raise Exception('wrong error code: {0}'.format(request.status_code))
+ raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
return request.json()['data']['photo']['id']
-class Activity(Generic):
+class Activity(Stream):
"""Stream representing user's activity.
"""
_location = 'activity.json'
"""
if type(post) == str: self._delid(post)
elif type(post) == Post: post.delete()
- else:
- raise TypeError('this method accepts str or Post types: {0} given')
+ else: raise TypeError('this method accepts str or Post types: {0} given')
self.fill()
An example call would be `aspects.json?aspect_ids=23,5,42`
"""
_location = 'aspects.json'
- _id_regexp = re.compile(r'<a href="/aspects/[0-9]+/edit" rel="facebox"')
- def getID(self, aspect):
+ def getAspectID(self, aspect_name):
"""Returns id of an aspect of given name.
Returns -1 if aspect is not found.
- :param aspect: aspect name (must be spelled exactly as when created)
- :type aspect: str
+ :param aspect_name: aspect name (must be spelled exactly as when created)
+ :type aspect_name: str
:returns: int
"""
id = -1
aspects = self._connection.getUserInfo()['aspects']
- for item in aspects:
- if item['name'] == aspect: id = item['id']
+ for aspect in aspects:
+ if aspect['name'] == aspect_name: id = aspect['id']
return id
def filterByIDs(self, ids):
self._location += '?{0}'.format(','.join(ids))
self.fill()
- def _getaid(self, response):
- """Extracts id of just created aspect.
- """
- id = self._id_regexp.search(response.text).group(0).split('/')[2]
- return int(id)
-
def add(self, aspect_name, visible=0):
"""This function adds a new aspect.
- Status code 422 is accepteb because it is returned by D* when
+ Status code 422 is accepted because it is returned by D* when
you try to add aspect already present on your aspect list.
- :returns: id of created aspect (or -1 if status_code was 422)
+ :returns: Aspect() object of just created aspect
"""
data = {'authenticity_token': self._connection.get_token(),
'aspect[name]': aspect_name,
if request.status_code not in [200, 422]:
raise Exception('wrong status code: {0}'.format(request.status_code))
- if request.status_code == 422: id = -1
- else: id = self._getaid(request)
- return id
+ id = self.getAspectID(aspect_name)
+ return Aspect(self._connection, id)
- def remove(self, aspect_id=0, name=''):
+ def remove(self, aspect_id=-1, name=''):
"""This method removes an aspect.
- 500 is accepted because although the D* will
+ You can give it either id or name of the aspect.
+ When both are specified, id takes precedence over name.
+
+ Status code 500 is accepted because although the D* will
go nuts it will remove the aspect anyway.
:param aspect_id: id fo aspect to remove
:param name: name of aspect to remove
:type name: str
"""
- if not aspect_id and name: aspect_id = self.getID(name)
- data = {'authenticity_token': self._connection.get_token()}
- request = self._connection.delete('aspects/{}'.format(aspect_id),
- data=data)
- if request.status_code not in [404, 500]:
- raise Exception('wrong status code: {0}'.format(request.status_code))
+ if aspect_id == -1 and name: aspect_id = self.getAspectID(name)
+ data = {'_method': 'delete',
+ 'authenticity_token': self._connection.get_token()}
+ request = self._connection.post('aspects/{0}'.format(aspect_id), data=data)
+ if request.status_code not in [200, 302, 500]:
+ raise Exception('wrong status code: {0}: cannot remove aspect'.format(request.status_code))
class Commented(Generic):
if request.status_code not in [201, 403]:
raise Exception('wrong error code: {0}'.format(request.status_code))
return request.status_code
+
+
+class Tag(Generic):
+ """This stream contains all posts containing a tag.
+ """
+ def __init__(self, connection, tag):
+ """
+ :param connection: Connection() object
+ :type connection: diaspy.connection.Connection
+ :param tag: tag name
+ :type tag: str
+ """
+ self._connection = connection
+ self._location = 'tags/{0}.json'.format(tag)
+ self.fill()