* __fix__: `diaspy.models.Post.__init__()` checking on different fetch states was a mess.
* __fix__: `diaspy.streams.Asepcts.filter()` location fix.
* __new__: `diaspy.tagFollowings.TagFollowings()` which represents the tags followed by the user.
* __new__: `diaspy.models.FollowedTag()` which represents a tag followed by the user. It is used by `diaspy.tagFollowings.TagFollowings()`.
* __new__: It is now possible to give `**requestKwargs` to `diaspy.connection.Connection()` which will be used for every `request` unless directly overwritten by given the function you call different key-worded arguments.
* __new__: `diaspy.connection.Connection()` now does check if the `pod` you are connecting to has `Camo` enabled or not. Call `diaspy.connection.Connection.camo()` to receive `True` or `False`.
* __new__: `diaspy.models.Comment.authordata()` which will return all author data instead of `diaspy.models.Comment.author()` which will only return data for a certain key.
* __new__: `diaspy.streams.Public()`
* __new__: `diaspy.models.Post.fetchlikes()`.
* __new__: `diaspy.models.Post.fetchreshares()`
* __rem__: `diaspy.streams.FollowedTags.get()` since it wasn’t doing anything usefull.
* __dep__: `diaspy.streams.FollowedTags.remove()` Use `diaspy.tagFollowings.TagFollowings[“tagName”].delete()` instead.
* __dep__: `diaspy.streams.FollowedTags.add()` Use diaspy.tagFollowings.TagFollowings.follow() instead.
----
+#### Version `0.6.1.dev` (not final changelog for this version, still in development)
+
+* __upd__: `diaspy.models.Post.like()`, `diaspy.models.Post.delete_like()`, `diaspy.models.Post.reshare()` will now update data (count and likes/reshares) without doing another request.
+
+* __fix__: `diaspy.models.Post.__init__()` checking on different fetch states was a mess.
+* __fix__: `diaspy.streams.Asepcts.filter()` location fix.
+
+* __new__: `diaspy.tagFollowings.TagFollowings()` which represents the tags followed by the user.
+* __new__: `diaspy.models.FollowedTag()` which represents a tag followed by the user. It is used by `diaspy.tagFollowings.TagFollowings()`.
+* __new__: It is now possible to give `**requestKwargs` to `diaspy.connection.Connection()` which will be used for every `request` unless directly overwritten by given the function you call different key-worded arguments.
+* __new__: `diaspy.connection.Connection()` now does check if the `pod` you are connecting to has `Camo` enabled or not. Call `diaspy.connection.Connection.camo()` to receive `True` or `False`.
+* __new__: `diaspy.models.Comment.authordata()` which will return all author data instead of `diaspy.models.Comment.author()` which will only return data for a certain key.
+* __new__: `diaspy.streams.Public()`
+* __new__: `diaspy.models.Post.fetchlikes()`.
+* __new__: `diaspy.models.Post.fetchreshares()`
+
+* __rem__: `diaspy.streams.FollowedTags.get()` since it wasn’t doing anything usefull.
+
+* __dep__: `diaspy.streams.FollowedTags.remove()` Use `diaspy.tagFollowings.TagFollowings[“tagName”].delete()` instead.
+* __dep__: `diaspy.streams.FollowedTags.add()` Use diaspy.tagFollowings.TagFollowings.follow() instead.
+
+
+----
+
+
#### Version `0.6.0`
In this release some bugs due to Diaspora changes where adressed, it also
import diaspy.people as people
import diaspy.notifications as notifications
import diaspy.settings as settings
+import diaspy.tagFollowings as tagFollowings
-__version__ = '0.6.0'
+__version__ = '0.6.1.dev'
_userinfo_regex_2 = re.compile(r'gon.user=({.*?});gon.')
_verify_SSL = True
- def __init__(self, pod, username, password, schema='https'):
+ def __init__(self, pod, username, password, schema='https', **requestsKwargs):
"""
:param pod: The complete url of the diaspora pod to use.
:type pod: str
:type username: str
:param password: The password used to log in.
:type password: str
+ :param requestsKwargs: default kwargs for requests (proxy, timeout, etc)
+ :type requestsKwargs: keyworded arguments
"""
self.pod = pod
self._session = requests.Session()
self._token = ''
self._diaspora_session = ''
self._fetch_token_from = 'stream'
+ self._requests_kwargs = requestsKwargs
+ self._camo_enabled = False
try: self._setlogin(username, password)
except requests.exceptions.MissingSchema:
self.pod = '{0}://{1}'.format(schema, self.pod)
warnings.warn('schema was missing')
+
try: self._setlogin(username, password)
except Exception as e:
raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
self._cookies = self._fetchcookies()
+ def __bool__(self):
+ if self._token: return True
+ return False
+
def _fetchcookies(self):
request = self.get('stream')
return request.cookies
"""
return self._fetchtoken()
+ def requestsKwargs(self):
+ """Returns keyworded arguments set to use for all requests.
+ """
+ return self._requests_kwargs
+
+ def setRequestsKwargs(self, **requestsKwargs):
+ """Sets keyworded arguments that will be used for earch request.
+ """
+ self._requests_kwargs = requestsKwargs
+
def get(self, string, headers={}, params={}, direct=False, **kwargs):
"""This method gets data from session.
Performs additional checks if needed.
"""
if not direct: url = '{0}/{1}'.format(self.pod, string)
else: url = string
+ if not kwargs: kwargs = self._requests_kwargs
return self._session.get(url, params=params, headers=headers, verify=self._verify_SSL, **kwargs)
def tokenFrom(self, location):
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
+ if not kwargs: kwargs = self._requests_kwargs
request = self._session.post(string, data, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
+ if not kwargs: kwargs = self._requests_kwargs
if data is not None: request = self._session.put(string, data, headers=headers, params=params, **kwargs)
else: request = self._session.put(string, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
string = '{0}/{1}'.format(self.pod, string)
if 'X-CSRF-Token' not in headers:
headers['X-CSRF-Token'] = self.get_token()
+ if not kwargs: kwargs = self._requests_kwargs
request = self._session.delete(string, data=data, headers=headers, verify=self._verify_SSL, **kwargs)
return request
+ def _checkCamo(self):
+ response = self._session.head("{0}/camo/".format(self.pod),
+ **self._requests_kwargs)
+ if response.status_code == 200: self._camo_enabled = True
+ else: self._camo_enabled = False
+
+ def camo(self): return self._camo_enabled;
+
def _setlogin(self, username, password):
"""This function is used to set data for login.
allow_redirects=False)
if request.status_code != 302:
raise errors.LoginError('{0}: login failed'.format(request.status_code))
+ self._checkCamo()
def login(self, remember_me=1):
"""This function is used to log in to a pod.
"""
return self._diaspora_session
- def userdata(self):
- return self._userdata
+ def userdata(self): return self._userdata
def getUserData(self):
"""Returns user data.
"""
return self._data['author'][key]
+ def authordata(self):
+ """Returns all author data of the comment.
+ """
+ return self._data['author']
+
class Comments():
def __init__(self, comments=[]):
self._comments = comments
self.guid = guid
self._data = {}
self.comments = Comments()
- if post_data:
- self._data = post_data
-
- if fetch: self._fetchdata()
+ if post_data: self._setdata(post_data)
+ elif fetch: self._fetchdata()
if comments:
if not self._data: self._fetchdata()
self._fetchcomments()
- else:
- if not self._data: self._fetchdata()
- self.comments.set_json( self.data()['interactions']['comments'] )
def __repr__(self):
"""Returns string containing more information then str().
"""
return self._data['text']
+ def _setdata(self, data):
+ self._data = data
+ if not bool(self.comments) and data['interactions'].get('comments', []):
+ self.comments.set_json(data['interactions'].get('comments', []))
+
def _fetchdata(self):
"""This function retrieves data of the post.
request = self._connection.get('posts/{0}.json'.format(id))
if request.status_code != 200:
raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
- elif request:
- self._data = request.json()
+ elif request: self._setdata(request.json());
return self.data()['guid']
def _fetchcomments(self):
else:
self.comments.set([Comment(c) for c in request.json()])
+ def _fetchlikes(self):
+ id = self.data()['id']
+ request = self._connection.get('posts/{0}/likes.json'.format(id))
+ if request.status_code != 200:
+ raise errors.PostError('{0}: could not fetch likes for post: {1}'.format(request.status_code, id))
+ json = request.json();
+ if json: self._data['interactions']['likes'] = request.json();
+ return self._data['interactions']['likes'];
+
+ def _fetchreshares(self):
+ id = self.data()['id']
+ request = self._connection.get('posts/{0}/reshares.json'.format(id))
+ if request.status_code != 200:
+ raise errors.PostError('{0}: could not fetch likes for post: {1}'.format(request.status_code, id))
+
+ json = request.json();
+ if json: self._data['interactions']['reshares'] = request.json();
+ return self._data['interactions']['reshares'];
+
+ def fetchlikes(self): return self._fetchlikes();
+ def fetchreshares(self): return self._fetchreshares();
+
def fetch(self, comments = False):
"""Fetches post data.
"""
self._fetchdata()
- if comments:
- self._fetchcomments()
+ if comments: self._fetchcomments()
return self
def data(self, data = None):
"""
data = {'authenticity_token': repr(self._connection)}
- request = self._connection.post('posts/{0}/likes'.format(self.id),
+ request = self._connection.post('posts/{0}/likes'.format(self.id),
data=data,
headers={'accept': 'application/json'})
likes_json = request.json()
if likes_json:
- self._data['interactions']['likes'] = [likes_json]
+ self._data['interactions']['likes'].insert(0, likes_json)
+ self._data['interactions']['likes_count'] = str(int(self._data['interactions']['likes_count'])+1)
return likes_json
def reshare(self):
headers={'accept': 'application/json'})
if request.status_code != 201:
raise Exception('{0}: Post could not be reshared'.format(request.status_code))
+
+ reshares_json = request.json()
+ if reshares_json:
+ self._data['interactions']['reshares'].insert(0, reshares_json)
+ self._data['interactions']['reshares_count'] = str(int(self._data['interactions']['reshares_count'])+1)
return request.json()
def comment(self, text):
def hide(self):
"""
- -> PUT /share_visibilities/42 HTTP/1.1
+ -> PUT /share_visibilities/42 HTTP/1.1
post_id=123
- <- HTTP/1.1 200 OK
+ <- HTTP/1.1 200 OK
"""
headers = {'x-csrf-token': repr(self._connection)}
params = {'post_id': json.dumps(self.id)}
def mute(self):
"""
- -> POST /blocks HTTP/1.1
+ -> POST /blocks HTTP/1.1
{"block":{"person_id":123}}
- <- HTTP/1.1 204 No Content
+ <- HTTP/1.1 204 No Content
"""
headers = {'content-type':'application/json', 'x-csrf-token': repr(self._connection)}
data = json.dumps({ 'block': { 'person_id' : self._data['author']['id'] } })
def subscribe(self):
"""
- -> POST /posts/123/participation HTTP/1.1
- <- HTTP/1.1 201 Created
+ -> POST /posts/123/participation HTTP/1.1
+ <- HTTP/1.1 201 Created
"""
headers = {'x-csrf-token': repr(self._connection)}
data = {}
def unsubscribe(self):
"""
- -> POST /posts/123/participation HTTP/1.1
+ -> POST /posts/123/participation HTTP/1.1
_method=delete
- <- HTTP/1.1 200 OK
+ <- HTTP/1.1 200 OK
"""
headers = {'x-csrf-token': repr(self._connection)}
data = { "_method": "delete" }
raise errors.PostError('{0}: Like could not be removed.'
.format(request.status_code))
+ self._data['interactions']['likes'].pop(0);
+ self._data['interactions']['likes_count'] = str(int(self._data['interactions']['likes_count'])-1)
+
def author(self, key='name'):
"""Returns author of the post.
:param key: all keys available in data['author']
"""
return self._data['author'][key]
+
+class FollowedTag():
+ """This class represents a followed tag.
+ `diaspy.tagFollowings.TagFollowings()` uses it.
+ """
+ def __init__(self, connection, id, name, taggings_count):
+ self._connection = connection
+ self._id, self._name, self._taggings_count = id, name, taggings_count
+
+ def id(self): return self._id
+ def name(self): return self._name
+ def count(self): return self._taggings_count
+
+ def delete(self):
+ data = {'authenticity_token': repr(self._connection)}
+ request = self._connection.delete('tag_followings/{0}'.format(self._id),
+ data=data,
+ headers={'accept': 'application/json'})
+ if request.status_code != 204:
+ raise errors.TagError('{0}: Tag could not be deleted.'
+ .format(request.status_code))
"""
return len(self._stream)
+ def __bool__(self):
+ """Returns True if stream os filled, False if not.
+ """
+ if self._stream: return True
+ return False
+
def _obtain(self, max_time=0, suppress=True):
"""Obtains stream from pod.
params['_'] = self.latest
request = self._connection.get(self._location, params=params)
if request.status_code != 200:
- raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
+ raise errors.StreamError('wrong status code: {0}'
+ .format(request.status_code))
posts = []
latest_time = None # Used to get the created_at from the latest posts we received.
for post in request.json():
try:
comments = False
if post['interactions']['comments_count'] > 3: comments = True
- posts.append(Post(self._connection, id=post['id'], guid=post['guid'], fetch=False, comments=comments, post_data=post))
+ posts.append(Post(self._connection, id=post['id'],
+ guid=post['guid'], fetch=False, comments=comments,
+ post_data=post))
if post['created_at']: latest_time = post['created_at']
except errors.PostError:
if not suppress:
"""
location = 'stream.json'
- def post(self, text='', aspect_ids='public', photos=None, photo='', poll_question=None, poll_answers=None, location_coords=None, provider_display_name=''):
+ def post(self, text='', aspect_ids='public', photos=None, photo='',
+ poll_question=None, poll_answers=None, location_coords=None,
+ provider_display_name=''):
"""This function sends a post to an aspect.
If both `photo` and `photos` are specified `photos` takes precedence.
"""
data = {}
data['aspect_ids'] = aspect_ids
- data['status_message'] = {'text': text, 'provider_display_name': provider_display_name}
+ data['status_message'] = ({'text': text,
+ 'provider_display_name': provider_display_name})
if photo: data['photos'] = self._photoupload(photo)
if photos: data['photos'] = photos
if poll_question and poll_answers:
if location_coords: data['location_coords'] = location_coords
request = self._connection.post('status_messages',
- data=json.dumps(data),
- headers={'content-type': 'application/json',
- 'accept': 'application/json',
- 'x-csrf-token': repr(self._connection)})
+ data=json.dumps(data),
+ headers={'content-type': 'application/json',
+ 'accept': 'application/json',
+ 'x-csrf-token': repr(self._connection)})
if request.status_code != 201:
- raise Exception('{0}: Post could not be posted.'.format(request.status_code))
+ raise Exception('{0}: Post could not be posted.'
+ .format(request.status_code))
post_json = request.json()
- post = Post(self._connection, id=post_json['id'], guid=post_json['guid'], post_data=post_json)
+ post = Post(self._connection, id=post_json['id'],
+ guid=post_json['guid'], post_data=post_json)
return post
def _photoupload(self, filename, aspects=[]):
'x-csrf-token': repr(self._connection),
'x-file-name': filename}
- request = self._connection.post('photos', data=image, params=params, headers=headers)
+ request = self._connection.post('photos', data=image, params=params,
+ headers=headers)
if request.status_code != 200:
- raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
+ raise errors.StreamError('photo cannot be uploaded: {0}'
+ .format(request.status_code))
return request.json()['data']['photo']['id']
:parameter ids: list of apsect ids
:type ids: list of integers
"""
- self._location = 'aspects.json?a_ids[]=' + '{}'.format('&a_ids[]='.join(ids))
+ self._location = 'aspects.json?a_ids[]=' + '{}'.format(
+ '&a_ids[]='.join(str(id) for id in ids))
self.fill() # this will create entirely new list of posts.
def add(self, aspect_name, visible=0):
request = self._connection.post('aspects', data=data)
if request.status_code not in [200, 422]:
- raise Exception('wrong status code: {0}'.format(request.status_code))
+ raise Exception('wrong status code: {0}'
+ .format(request.status_code))
id = self.getAspectID(aspect_name)
return Aspect(self._connection, id)
'authenticity_token': repr(self._connection)}
request = self._connection.post('aspects/{0}'.format(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))
+ raise Exception('wrong status code: {0}: cannot remove aspect'
+ .format(request.status_code))
class Commented(Generic):
_location = 'mentions.json'
+class Public(Generic):
+ """Public stream.
+ """
+ _location = 'public.json'
+
+
class FollowedTags(Generic):
"""This stream contains all posts
containing tags the user is following.
"""
_location = 'followed_tags.json'
- def get(self):
- """Returns list of followed tags.
- """
- return []
-
def remove(self, tag_id):
"""Stop following a tag.
:param tag_id: tag id
:type tag_id: int
+
+ FIXME:
+ Deprecated, this function will be removed in next version.
+ Use diaspy.tagFollowings.TagFollowings[“tagName”].delete() instead.
"""
data = {'authenticity_token': self._connection.get_token()}
- request = self._connection.delete('tag_followings/{0}'.format(tag_id), data=data)
+ request = self._connection.delete('tag_followings/{0}'
+ .format(tag_id), data=data)
if request.status_code != 404:
- raise Exception('wrong status code: {0}'.format(request.status_code))
+ raise Exception('wrong status code: {0}'
+ .format(request.status_code))
def add(self, tag_name):
"""Follow new tag.
:param tag_name: tag name
:type tag_name: str
:returns: int (response code)
+
+ FIXME:
+ Deprecated, this function will be removed in next version.
+ Use diaspy.tagFollowings.TagFollowings.follow() instead.
"""
data = {'name': tag_name,
'authenticity_token': repr(self._connection),
'accept': 'application/json'
}
- request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
+ request = self._connection.post('tag_followings', data=json.dumps(data),
+ headers=headers)
if request.status_code not in [201, 403]:
raise Exception('wrong error code: {0}'.format(request.status_code))
:param tag: tag name
:type tag: str
"""
- self._connection = connection
- self._location = 'tags/{0}.json'.format(tag)
- if fetch: self.fill()
+ super().__init__(connection, 'tags/{0}.json'.format(tag), fetch)
--- /dev/null
+#!/usr/bin/env python3
+from diaspy.models import FollowedTag
+from diaspy import errors
+class TagFollowings():
+ """This class represents the tags followed by the user.
+
+ Should return `dict`s in a `list` with the following keys:
+ `id`, `name`, `taggings_count`
+ """
+ def __init__(self, connection, fetch=True):
+ self._connection = connection
+ self._tags = []
+ if fetch: self.fetch()
+
+ def __iter__(self): return iter(self._tags)
+
+ def __getitem__(self, t): return self._tags[t]
+
+ def _finalise(self, tags):
+ return([FollowedTag(self._connection, t['id'], t['name'],
+ t['taggings_count']) for t in tags])
+
+ def fetch(self):
+ """(Re-)Fetches your followed tags.
+ """
+ self._tags = self.get()
+
+ def follow(self, name):
+ """Follows a tag by given name.
+
+ Returns FollowedTag object.
+ """
+ data = {'authenticity_token': repr(self._connection)}
+ params = {'name': name}
+ request = self._connection.post('tag_followings', data=data,
+ params=params, headers={'accept': 'application/json'})
+ if request.status_code != 201:
+ raise errors.TagError('{0}: Tag could not be followed.'
+ .format(request.status_code))
+ result = request.json()
+ self._tags.append(FollowedTag(self._connection, result['id'],
+ result['name'], result['taggings_count']))
+ return self._tags[(len(self._tags) - 1)]
+
+ def get(self):
+ request = self._connection.get('tag_followings.json')
+ if request.status_code != 200:
+ raise Exception('status code: {0}: cannot retreive tag_followings'
+ .format(request.status_code))
+ return self._finalise(request.json())
# built documents.
#
# The short X.Y version.
-version = '0.6.0'
+version = '0.6.1'
# The full version, including alpha/beta/rc tags.
-release = '0.6.0'
+release = '0.6.1.dev'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
setup(
name='diaspy-api',
- version='0.6.0',
+ version='0.6.1.dev',
author='Marek Marecki',
author_email='marekjm@ozro.pw',
url='https://github.com/marekjm/diaspy',