Code fixes, new features and refactoring in the field of user data and
authorMarek Marecki <marekjm@taistelu.com>
Wed, 13 Nov 2013 21:00:08 +0000 (22:00 +0100)
committerMarek Marecki <marekjm@taistelu.com>
Wed, 13 Nov 2013 21:00:08 +0000 (22:00 +0100)
info

Changelog.markdown
diaspy/connection.py
diaspy/errors.py
diaspy/models.py
diaspy/people.py
diaspy/settings.py
diaspy/streams.py
tests.py

index 3c80caf677665c3d9a7f2868052062596ca7ce2e..3e2742b9209a09c78901b972ff4224afc499e57a 100644 (file)
@@ -34,6 +34,7 @@ This version has some small incompatibilities with `0.4.1` so read Changelog car
 * __new__:  `diaspy.people.User._fetchstream()` method,
 * __new__:  `diaspy.people.Me()` object representing current user,
 * __new__:  `**kwargs` added to `diaspy.streams.Generic.json()` methdo to give developers control over the creation of JSON,
+* __new__:  `.getHCard()` method added to `diaspy.people.User()`,
 
 
 * __upd__:  `diaspy.connection.Connection.login()` modifies connection object in-place **and** returns it (this allows more fluent API),
@@ -41,10 +42,12 @@ This version has some small incompatibilities with `0.4.1` so read Changelog car
 * __upd__:  `diaspy.connection.Connection._login()` no longer returns status code (if login was unsuccessful it'll raise an exception),
 * __upd__:  better error message in `diaspy.models.Post().__init__()`,
 * __upd__:  `data` variable in `diaspy.models.Post()` renamed to `_data` to indicate that it's considered private,
+* __upd__:  after deleting a post `Activity` stream is purged instead of being refilled (this preserves state of stream which is not reset to last 15 posts),
+* __upd__:  `filterByIDs()` method in `Aspects` stream renamed to `filter()`,
 
 
-* __rem__:  `diaspy.connection.Connection.getUserInfo()` moved to `diaspy.people.Me.getInfo()`,
-* __rem__:  `diaspy.people.Me.getInfo()` (moved from `diaspy.connection.Connection.getUserInfo()`),
+* __rem__:  `diaspy.connection.Connection.getUserInfo()` moved to `diaspy.connection.Connection.getUserData()`,
+* __rem__:  `fetch` parameter removed from `diaspy.connection.Connection.getUserData()`,
 
 
 * __dep__:  `max_time` parameter in `diaspy.streams.*.more()` method is deprecated,
index 12c035a4679553d8840943bce3f1dff5eb43ad7a..67dbabb373ae4af8e4e2b622bd0e985310e0c1d7 100644 (file)
@@ -20,6 +20,9 @@ class Connection():
     """Object representing connection with the pod.
     """
     _token_regex = re.compile(r'content="(.*?)"\s+name="csrf-token')
+    _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
+    # this is for older version of D*
+    _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
 
     def __init__(self, pod, username, password, schema='https'):
         """
@@ -205,3 +208,13 @@ class Connection():
         """Returns session token string (_diaspora_session).
         """
         return self._diaspora_session
+
+    def getUserData(self):
+        """Returns user data.
+        """
+        request = self.get('bookmarklet')
+        userdata = self._userinfo_regex.search(request.text)
+        if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
+        if userdata is None: raise errors.DiaspyError('cannot find user data')
+        userdata = userdata.group(1)
+        return json.loads(userdata)
index 1b2a5b1ba086766233a7ea50a04327c5e7075551..6fd8c24c0c6f7a9da1052a45e6efd6a7667199d9 100644 (file)
@@ -1,5 +1,25 @@
 #!/usr/bin/env python3
 
+"""This module contains custom exceptions that are raised by diaspy.
+These are not described by DIASPORA* protocol as exceptions that should be
+raised by API implementations but are specific to this particular implementation.
+
+If your program should catch all exceptions raised by diaspy and
+does not need to handle them specifically you can use following code:
+
+    # this line imports all errors
+    from diaspy.errors import *
+
+    try:
+        # your code...
+    except DiaspyError as e:
+        # your error handling code...
+    finally:
+        # closing code...
+"""
+
+import warnings
+
 
 class DiaspyError(Exception):
     """Base exception for all errors
@@ -16,7 +36,7 @@ class LoginError(DiaspyError):
     pass
 
 
-class TokenError(Exception):
+class TokenError(DiaspyError):
     pass
 
 
@@ -94,6 +114,7 @@ def react(r, message='', accepted=[200, 201, 202, 203, 204, 205, 206], exception
     :param exception: preferred exception to raise
     :type exception: valid exception type (default: DiaspyError)
     """
+    warnings.warn(DeprecationWarning)
     if r in accepted: e = None
     else: e = DiaspyError
 
@@ -111,5 +132,6 @@ def throw(e, message=''):
     :param message: message for exception
     :type message: str
     """
+    warnings.warn(DeprecationWarning)
     if e is None: pass
     else: raise e(message)
index a8d1c1767b26152a059bd08f6f94d396ad4bc01d..4d7b514d77256f037211b19269e0afc4ec8a1a89 100644 (file)
@@ -33,7 +33,7 @@ class Aspect():
         """Finds name for aspect.
         """
         name = None
-        aspects = self._connection.getUserInfo()['aspects']
+        aspects = self._connection.getUserData()['aspects']
         for a in aspects:
             if a['id'] == self.id:
                 name = a['name']
@@ -44,7 +44,7 @@ class Aspect():
         """Finds id for aspect.
         """
         id = None
-        aspects = self._connection.getUserInfo()['aspects']
+        aspects = self._connection.getUserData()['aspects']
         for a in aspects:
             if a['name'] == self.name:
                 id = a['id']
@@ -342,7 +342,7 @@ class Post():
     def __repr__(self):
         """Returns string containing more information then str().
         """
-        return '{0} ({1}): {2}'.format(self._data['author']['name'], self.data['author']['guid'], self.data['text'])
+        return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
 
     def __str__(self):
         """Returns text of a post.
@@ -373,9 +373,10 @@ class Post():
 
     def _fetchcomments(self):
         """Retreives comments for this post.
+        Retrieving comments via GUID will result in 404 error.
+        DIASPORA* does not supply comments through /posts/:guid/ endpoint.
         """
-        if self.id: id = self.id
-        if self.guid: id = self.guid
+        id = self._data['id']
         if self['interactions']['comments_count']:
             request = self._connection.get('posts/{0}/comments.json'.format(id))
             if request.status_code != 200:
index 76a1913bcdeeba09aa4e7b3313c139267af12746..a347ecf54c0d7d257e74e2cd8a5753fa1976c2c8 100644 (file)
@@ -119,6 +119,13 @@ class User():
         data = search.Search(self._connection).user(self['handle'])[0]
         self.data = data
 
+    def getHCard(self):
+        """Returns XML string containing user HCard.
+        """
+        request = self._connection.get('hcard/users/{0}'.format(self['guid']))
+        if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
+        return request.text
+
 
 class Me():
     """Object represetnting current user.
@@ -129,7 +136,7 @@ class Me():
     def __init__(self, connection):
         self._connection = connection
 
-    def getInfo(self, fetch=False):
+    def getInfo(self):
         """This function returns the current user's attributes.
 
         :returns: dict -- json formatted user info.
@@ -142,7 +149,6 @@ class Me():
         return json.loads(userdata)
 
 
-
 class Contacts():
     """This class represents user's list of contacts.
     """
index ded22589228a3ebdb5a6beb3a9b53e370cd5e402..4e7971b1dcd46d6318c3a4980439426de03ba203 100644 (file)
@@ -168,7 +168,7 @@ class Profile():
     def getTags(self):
         """Returns tags user had selected when describing him/her-self.
         """
-        guid = self._connection.getUserInfo()['guid']
+        guid = self._connection.getUserData()['guid']
         html = self._connection.get('people/{0}'.format(guid)).text
         description_regexp = re.compile('<a href="/tags/(.*?)" class="tag">#.*?</a>')
         return [tag.lower() for tag in re.findall(description_regexp, html)]
index 66d527c0d9b5fff32d9f9acc1bb642b82eea8125..6878c5eaf9a40dad0ebecf4f461d6a00e6d7ee7f 100644 (file)
@@ -60,7 +60,7 @@ class Generic():
         if max_time:
             params['max_time'] = max_time
             params['_'] = int(time.time() * 1000)
-        request = self._connection.get(self._location, params=params, headers={'cookie': ''})
+        request = self._connection.get(self._location, params=params)
         if request.status_code != 200:
             raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
         return [Post(self._connection, guid=post['guid']) for post in request.json()]
@@ -100,8 +100,8 @@ class Generic():
         for post in self._stream:
             deleted = False
             try:
+                # error will tell us that the post has been deleted
                 post.update()
-                stream.append(post)
             except Exception:
                 deleted = True
             finally:
@@ -263,11 +263,13 @@ class Stream(Generic):
         post = Post(self._connection, request.json()['id'])
         return post
 
-    def _photoupload(self, filename):
+    def _photoupload(self, filename, aspects=[]):
         """Uploads picture to the pod.
 
         :param filename: path to picture file
         :type filename: str
+        :param aspect_ids: list of ids of aspects to which you want to upload this photo
+        :type aspect_ids: list of integers
 
         :returns: id of the photo being uploaded
         """
@@ -279,7 +281,7 @@ class Stream(Generic):
         params['photo[pending]'] = 'true'
         params['set_profile_image'] = ''
         params['qqfile'] = filename
-        aspects = self._connection.getUserInfo()['aspects']
+        if not aspects: aspects = self._connection.getUserData()['aspects']
         for i, aspect in enumerate(aspects):
             params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
 
@@ -312,7 +314,7 @@ class Activity(Stream):
         """Deletes post from users activity.
         `post` can be either post id or Post()
         object which will be identified and deleted.
-        After deleting post the stream will be filled.
+        After deleting post the stream will be purged.
 
         :param post: post identifier
         :type post: str, diaspy.models.Post
@@ -320,7 +322,7 @@ class Activity(Stream):
         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')
-        self.fill()
+        self.purge()
 
 
 class Aspects(Generic):
@@ -342,13 +344,18 @@ class Aspects(Generic):
         :returns: int
         """
         id = -1
-        aspects = self._connection.getUserInfo()['aspects']
+        aspects = self._connection.getUserData()['aspects']
         for aspect in aspects:
             if aspect['name'] == aspect_name: id = aspect['id']
         return id
 
-    def filterByIDs(self, ids):
-        self._location += '?{0}'.format(','.join(ids))
+    def filter(self, ids):
+        """Filters posts by given aspect ids.
+
+        :parameter ids: list of apsect ids
+        :type ids: list of integers
+        """
+        self._location = 'aspects.json' + '?{0}'.format(','.join(ids))
         self.fill()
 
     def add(self, aspect_name, visible=0):
@@ -419,6 +426,11 @@ class FollowedTags(Generic):
     """
     _location = 'followed_tags.json'
 
+    def get(self):
+        """Returns list of followed tags.
+        """
+        return []
+
     def remove(self, tag_id):
         """Stop following a tag.
 
@@ -457,7 +469,7 @@ class FollowedTags(Generic):
 class Tag(Generic):
     """This stream contains all posts containing a tag.
     """
-    def __init__(self, connection, tag):
+    def __init__(self, connection, tag, fetch=True):
         """
         :param connection: Connection() object
         :type connection: diaspy.connection.Connection
@@ -466,4 +478,4 @@ class Tag(Generic):
         """
         self._connection = connection
         self._location = 'tags/{0}.json'.format(tag)
-        self.fill()
+        if fetch: self.fill()
index 156f405131e24dbb5841a620ddd5f0a9d85f845f..2c74e4d4d6a1cd92d26503c30450870f237a8206 100644 (file)
--- a/tests.py
+++ b/tests.py
@@ -10,7 +10,6 @@ import requests
 import warnings
 #   actual diaspy code
 import diaspy
-from diaspy import client as dclient
 
 
 ####    SETUP STUFF
@@ -46,7 +45,7 @@ diaspy.streams.Aspects(test_connection).add(testconf.test_aspect_name_fake)
 testconf.test_aspect_id = diaspy.streams.Aspects(test_connection).add(testconf.test_aspect_name).id
 print('OK')
 
-print([i['name'] for i in test_connection.getUserInfo()['aspects']])
+print([i['name'] for i in test_connection.getUserData()['aspects']])
 
 
 post_text = '#diaspy test no. {0}'.format(test_count)
@@ -57,7 +56,7 @@ post_text = '#diaspy test no. {0}'.format(test_count)
 #######################################
 class ConnectionTest(unittest.TestCase):
     def testGettingUserInfo(self):
-        info = test_connection.getUserInfo()
+        info = test_connection.getUserData()
         self.assertEqual(dict, type(info))
 
 
@@ -77,12 +76,12 @@ class AspectsTests(unittest.TestCase):
 
     def testAspectsRemoveById(self):
         aspects = diaspy.streams.Aspects(test_connection)
-        for i in test_connection.getUserInfo()['aspects']:
+        for i in test_connection.getUserData()['aspects']:
             if i['name'] == testconf.test_aspect_name:
                 print(i['id'], end=' ')
                 aspects.remove(id=i['id'])
                 break
-        names = [i['name'] for i in test_connection.getUserInfo()['aspects']]
+        names = [i['name'] for i in test_connection.getUserData()['aspects']]
         print(names)
         self.assertNotIn(testconf.test_aspect_name, names)
 
@@ -90,7 +89,7 @@ class AspectsTests(unittest.TestCase):
         aspects = diaspy.streams.Aspects(test_connection)
         print(testconf.test_aspect_name_fake, end=' ')
         aspects.remove(name=testconf.test_aspect_name_fake)
-        names = [i['name'] for i in test_connection.getUserInfo()['aspects']]
+        names = [i['name'] for i in test_connection.getUserData()['aspects']]
         print(names)
         self.assertNotIn(testconf.test_aspect_name_fake, names)
 
@@ -260,4 +259,4 @@ if __name__ == '__main__':
     print('It\'s testing time!')
     n = unittest.main()
     print(n)
-    print('It was testing time!')
+    print('Good! All tests passed!')