Change indentation from spaces to tabs as discussed here https://github.com/marekjm...
authorCYBERDEViLNL <CYBERDEViLNL@github.com>
Fri, 26 Oct 2018 17:05:03 +0000 (19:05 +0200)
committerCYBERDEViLNL <CYBERDEViLNL@github.com>
Fri, 26 Oct 2018 17:05:03 +0000 (19:05 +0200)
diaspy/connection.py
diaspy/conversations.py
diaspy/errors.py
diaspy/models.py
diaspy/notifications.py
diaspy/people.py
diaspy/search.py
diaspy/settings.py
diaspy/streams.py

index 17d92d9479834a42cef115efbd51d30381389da7..95597066b6c51f37059cf40163272324181bfa8d 100644 (file)
@@ -18,238 +18,238 @@ DEBUG = True
 
 
 class Connection():
-    """Object representing connection with the pod.
-    """
-    _token_regex = re.compile(r'name="csrf-token"\s+content="(.*?)"')
-    _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
-    # this is for older version of D*
-    _token_regex_2 = re.compile(r'content="(.*?)"\s+name="csrf-token')
-    _userinfo_regex_2 = re.compile(r'gon.user=({.*?});gon.')
-    _verify_SSL = True
-
-    def __init__(self, pod, username, password, schema='https'):
-        """
-        :param pod: The complete url of the diaspora pod to use.
-        :type pod: str
-        :param username: The username used to log in.
-        :type username: str
-        :param password: The password used to log in.
-        :type password: str
-        """
-        self.pod = pod
-        self._session = requests.Session()
-        self._login_data = {'user[remember_me]': 1, 'utf8': '✓'}
-        self._userdata = {}
-        self._token = ''
-        self._diaspora_session = ''
-        self._cookies = self._fetchcookies()
-        self._fetch_token_from = 'stream'
-        try:
-            #self._setlogin(username, password)
-            self._login_data = {'user[username]': username,
-                                'user[password]': password,
-                                'authenticity_token': self._fetchtoken()}
-            success = True
-        except requests.exceptions.MissingSchema:
-            self.pod = '{0}://{1}'.format(schema, self.pod)
-            warnings.warn('schema was missing')
-            success = False
-        finally:
-            pass
-        try:
-            if not success:
-                self._login_data = {'user[username]': username,
-                                    'user[password]': password,
-                                    'authenticity_token': self._fetchtoken()}
-        except Exception as e:
-            raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
-
-    def _fetchcookies(self):
-        request = self.get('stream')
-        return request.cookies
-
-    def __repr__(self):
-        """Returns token string.
-        It will be easier to change backend if programs will just use:
-            repr(connection)
-        instead of calling a specified method.
-        """
-        return self._fetchtoken()
-
-    def get(self, string, headers={}, params={}, direct=False, **kwargs):
-        """This method gets data from session.
-        Performs additional checks if needed.
-
-        Example:
-            To obtain 'foo' from pod one should call `get('foo')`.
-
-        :param string: URL to get without the pod's URL and slash eg. 'stream'.
-        :type string: str
-        :param direct: if passed as True it will not be expanded
-        :type direct: bool
-        """
-        if not direct: url = '{0}/{1}'.format(self.pod, string)
-        else: url = string
-        return self._session.get(url, params=params, headers=headers, verify=self._verify_SSL, **kwargs)
-
-    def tokenFrom(self, location):
-        """Sets location for the *next* fetch of CSRF token.
-        Intended to be used for oneliners like this one:
-
-            connection.tokenFrom('somewhere').delete(...)
-
-        where the token comes from "somewhere" instead of the
-        default stream page.
-        """
-        self._fetch_token_from = location
-        return self
-
-    def post(self, string, data, headers={}, params={}, **kwargs):
-        """This method posts data to session.
-        Performs additional checks if needed.
-
-        Example:
-            To post to 'foo' one should call `post('foo', data={})`.
-
-        :param string: URL to post without the pod's URL and slash eg. 'status_messages'.
-        :type string: str
-        :param data: Data to post.
-        :param headers: Headers (optional).
-        :type headers: dict
-        :param params: Parameters (optional).
-        :type params: dict
-        """
-        string = '{0}/{1}'.format(self.pod, string)
-        if 'X-CSRF-Token' not in headers:
-            headers['X-CSRF-Token'] = self.get_token()
-        request = self._session.post(string, data, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
-        return request
-
-    def put(self, string, data=None, headers={}, params={}, **kwargs):
-        """This method PUTs to session.
-        """
-        string = '{0}/{1}'.format(self.pod, string)
-        if 'X-CSRF-Token' not in headers:
-            headers['X-CSRF-Token'] = self.get_token()
-        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
-
-    def delete(self, string, data = None, headers={}, **kwargs):
-        """This method lets you send delete request to session.
-        Performs additional checks if needed.
-
-        :param string: URL to use.
-        :type string: str
-        :param data: Data to use.
-        :param headers: Headers to use (optional).
-        :type headers: dict
-        """
-        string = '{0}/{1}'.format(self.pod, string)
-        if 'X-CSRF-Token' not in headers:
-            headers['X-CSRF-Token'] = self.get_token()
-        request = self._session.delete(string, data=data, headers=headers, verify=self._verify_SSL, **kwargs)
-        return request
-
-    def _setlogin(self, username, password):
-        """This function is used to set data for login.
-
-        .. note::
-            It should be called before _login() function.
-        """
-        self._login_data = {'user[username]': username,
-                            'user[password]': password,
-                            'authenticity_token': self._fetchtoken()}
-
-    def _login(self):
-        """Handles actual login request.
-        Raises LoginError if login failed.
-        """
-        request = self.post('users/sign_in',
-                            data=self._login_data,
-                            allow_redirects=False)
-        if request.status_code != 302:
-            raise errors.LoginError('{0}: login failed'.format(request.status_code))
-
-    def login(self, remember_me=1):
-        """This function is used to log in to a pod.
-        Will raise LoginError if password or username was not specified.
-        """
-        if not self._login_data['user[username]'] or not self._login_data['user[password]']:
-            raise errors.LoginError('username and/or password is not specified')
-        self._login_data['user[remember_me]'] = remember_me
-        status = self._login()
-        self._login_data = {}
-        return self
-
-    def logout(self):
-        """Logs out from a pod.
-        When logged out you can't do anything.
-        """
-        self.get('users/sign_out')
-        self.token = ''
-
-    def podswitch(self, pod, username, password):
-        """Switches pod from current to another one.
-        """
-        self.pod = pod
-        self._setlogin(username, password)
-        self._login()
-
-    def _fetchtoken(self):
-        """This method tries to get token string needed for authentication on D*.
-
-        :returns: token string
-        """
-        request = self.get(self._fetch_token_from)
-        token = self._token_regex.search(request.text)
-        if token is None: token = self._token_regex_2.search(request.text)
-        if token is not None: token = token.group(1)
-        else: raise errors.TokenError('could not find valid CSRF token')
-        self._token = token
-        self._fetch_token_from = 'stream'
-        return token
-
-    def get_token(self, fetch=True):
-        """This function returns a token needed for authentication in most cases.
-        **Notice:** using repr() is recommended method for getting token.
-
-        Each time it is run a _fetchtoken() is called and refreshed token is stored.
-
-        It is more safe to use than _fetchtoken().
-        By setting new you can request new token or decide to get stored one.
-        If no token is stored new one will be fetched anyway.
-
-        :returns: string -- token used to authenticate
-        """
-        try:
-            if fetch or not self._token: self._fetchtoken()
-        except requests.exceptions.ConnectionError as e:
-            warnings.warn('{0} was cought: reusing old token'.format(e))
-        finally:
-            if not self._token: raise errors.TokenError('cannot obtain token and no previous token found for reuse')
-        return self._token
-
-    def getSessionToken(self):
-        """Returns session token string (_diaspora_session).
-        """
-        return self._diaspora_session
-
-    def userdata(self):
-        return self._userdata
-
-    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)
-        self._userdata = json.loads(userdata)
-        return self._userdata
-
-    def set_verify_SSL(self, verify):
-        """Sets whether there should be an error if a SSL-Certificate could not be verified.
-        """
-        self._verify_SSL = verify
+       """Object representing connection with the pod.
+       """
+       _token_regex = re.compile(r'name="csrf-token"\s+content="(.*?)"')
+       _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
+       # this is for older version of D*
+       _token_regex_2 = re.compile(r'content="(.*?)"\s+name="csrf-token')
+       _userinfo_regex_2 = re.compile(r'gon.user=({.*?});gon.')
+       _verify_SSL = True
+
+       def __init__(self, pod, username, password, schema='https'):
+               """
+               :param pod: The complete url of the diaspora pod to use.
+               :type pod: str
+               :param username: The username used to log in.
+               :type username: str
+               :param password: The password used to log in.
+               :type password: str
+               """
+               self.pod = pod
+               self._session = requests.Session()
+               self._login_data = {'user[remember_me]': 1, 'utf8': '✓'}
+               self._userdata = {}
+               self._token = ''
+               self._diaspora_session = ''
+               self._cookies = self._fetchcookies()
+               self._fetch_token_from = 'stream'
+               try:
+                       #self._setlogin(username, password)
+                       self._login_data = {'user[username]': username,
+                                                               'user[password]': password,
+                                                               'authenticity_token': self._fetchtoken()}
+                       success = True
+               except requests.exceptions.MissingSchema:
+                       self.pod = '{0}://{1}'.format(schema, self.pod)
+                       warnings.warn('schema was missing')
+                       success = False
+               finally:
+                       pass
+               try:
+                       if not success:
+                               self._login_data = {'user[username]': username,
+                                                                       'user[password]': password,
+                                                                       'authenticity_token': self._fetchtoken()}
+               except Exception as e:
+                       raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
+
+       def _fetchcookies(self):
+               request = self.get('stream')
+               return request.cookies
+
+       def __repr__(self):
+               """Returns token string.
+               It will be easier to change backend if programs will just use:
+                       repr(connection)
+               instead of calling a specified method.
+               """
+               return self._fetchtoken()
+
+       def get(self, string, headers={}, params={}, direct=False, **kwargs):
+               """This method gets data from session.
+               Performs additional checks if needed.
+
+               Example:
+                       To obtain 'foo' from pod one should call `get('foo')`.
+
+               :param string: URL to get without the pod's URL and slash eg. 'stream'.
+               :type string: str
+               :param direct: if passed as True it will not be expanded
+               :type direct: bool
+               """
+               if not direct: url = '{0}/{1}'.format(self.pod, string)
+               else: url = string
+               return self._session.get(url, params=params, headers=headers, verify=self._verify_SSL, **kwargs)
+
+       def tokenFrom(self, location):
+               """Sets location for the *next* fetch of CSRF token.
+               Intended to be used for oneliners like this one:
+
+                       connection.tokenFrom('somewhere').delete(...)
+
+               where the token comes from "somewhere" instead of the
+               default stream page.
+               """
+               self._fetch_token_from = location
+               return self
+
+       def post(self, string, data, headers={}, params={}, **kwargs):
+               """This method posts data to session.
+               Performs additional checks if needed.
+
+               Example:
+                       To post to 'foo' one should call `post('foo', data={})`.
+
+               :param string: URL to post without the pod's URL and slash eg. 'status_messages'.
+               :type string: str
+               :param data: Data to post.
+               :param headers: Headers (optional).
+               :type headers: dict
+               :param params: Parameters (optional).
+               :type params: dict
+               """
+               string = '{0}/{1}'.format(self.pod, string)
+               if 'X-CSRF-Token' not in headers:
+                       headers['X-CSRF-Token'] = self.get_token()
+               request = self._session.post(string, data, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
+               return request
+
+       def put(self, string, data=None, headers={}, params={}, **kwargs):
+               """This method PUTs to session.
+               """
+               string = '{0}/{1}'.format(self.pod, string)
+               if 'X-CSRF-Token' not in headers:
+                       headers['X-CSRF-Token'] = self.get_token()
+               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
+
+       def delete(self, string, data = None, headers={}, **kwargs):
+               """This method lets you send delete request to session.
+               Performs additional checks if needed.
+
+               :param string: URL to use.
+               :type string: str
+               :param data: Data to use.
+               :param headers: Headers to use (optional).
+               :type headers: dict
+               """
+               string = '{0}/{1}'.format(self.pod, string)
+               if 'X-CSRF-Token' not in headers:
+                       headers['X-CSRF-Token'] = self.get_token()
+               request = self._session.delete(string, data=data, headers=headers, verify=self._verify_SSL, **kwargs)
+               return request
+
+       def _setlogin(self, username, password):
+               """This function is used to set data for login.
+
+               .. note::
+                       It should be called before _login() function.
+               """
+               self._login_data = {'user[username]': username,
+                                                       'user[password]': password,
+                                                       'authenticity_token': self._fetchtoken()}
+
+       def _login(self):
+               """Handles actual login request.
+               Raises LoginError if login failed.
+               """
+               request = self.post('users/sign_in',
+                                                       data=self._login_data,
+                                                       allow_redirects=False)
+               if request.status_code != 302:
+                       raise errors.LoginError('{0}: login failed'.format(request.status_code))
+
+       def login(self, remember_me=1):
+               """This function is used to log in to a pod.
+               Will raise LoginError if password or username was not specified.
+               """
+               if not self._login_data['user[username]'] or not self._login_data['user[password]']:
+                       raise errors.LoginError('username and/or password is not specified')
+               self._login_data['user[remember_me]'] = remember_me
+               status = self._login()
+               self._login_data = {}
+               return self
+
+       def logout(self):
+               """Logs out from a pod.
+               When logged out you can't do anything.
+               """
+               self.get('users/sign_out')
+               self.token = ''
+
+       def podswitch(self, pod, username, password):
+               """Switches pod from current to another one.
+               """
+               self.pod = pod
+               self._setlogin(username, password)
+               self._login()
+
+       def _fetchtoken(self):
+               """This method tries to get token string needed for authentication on D*.
+
+               :returns: token string
+               """
+               request = self.get(self._fetch_token_from)
+               token = self._token_regex.search(request.text)
+               if token is None: token = self._token_regex_2.search(request.text)
+               if token is not None: token = token.group(1)
+               else: raise errors.TokenError('could not find valid CSRF token')
+               self._token = token
+               self._fetch_token_from = 'stream'
+               return token
+
+       def get_token(self, fetch=True):
+               """This function returns a token needed for authentication in most cases.
+               **Notice:** using repr() is recommended method for getting token.
+
+               Each time it is run a _fetchtoken() is called and refreshed token is stored.
+
+               It is more safe to use than _fetchtoken().
+               By setting new you can request new token or decide to get stored one.
+               If no token is stored new one will be fetched anyway.
+
+               :returns: string -- token used to authenticate
+               """
+               try:
+                       if fetch or not self._token: self._fetchtoken()
+               except requests.exceptions.ConnectionError as e:
+                       warnings.warn('{0} was cought: reusing old token'.format(e))
+               finally:
+                       if not self._token: raise errors.TokenError('cannot obtain token and no previous token found for reuse')
+               return self._token
+
+       def getSessionToken(self):
+               """Returns session token string (_diaspora_session).
+               """
+               return self._diaspora_session
+
+       def userdata(self):
+               return self._userdata
+
+       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)
+               self._userdata = json.loads(userdata)
+               return self._userdata
+
+       def set_verify_SSL(self, verify):
+               """Sets whether there should be an error if a SSL-Certificate could not be verified.
+               """
+               self._verify_SSL = verify
index cd141ec167718a3923c5a274bc08ed88ae84cafb..8a59ceb665ec266378043eeefd27aace3a478216 100644 (file)
@@ -5,28 +5,28 @@ from diaspy import errors, models
 
 
 class Mailbox():
-    """Object implementing diaspora* mailbox.
-    """
-    def __init__(self, connection, fetch=True):
-        self._connection = connection
-        self._mailbox = []
-        if fetch: self._fetch()
-
-    def __len__(self):
-        return len(self._mailbox)
-
-    def __iter__(self):
-        return iter(self._mailbox)
-
-    def __getitem__(self, n):
-        return self._mailbox[n]
-
-    def _fetch(self):
-        """This method will fetch messages from user's mailbox.
-        """
-        request = self._connection.get('conversations.json')
-
-        if request.status_code != 200:
-            raise errors.DiaspyError('wrong status code: {0}'.format(request.status_code))
-        mailbox = request.json()
-        self._mailbox = [models.Conversation(self._connection, c['conversation']['id']) for c in mailbox]
+       """Object implementing diaspora* mailbox.
+       """
+       def __init__(self, connection, fetch=True):
+               self._connection = connection
+               self._mailbox = []
+               if fetch: self._fetch()
+
+       def __len__(self):
+               return len(self._mailbox)
+
+       def __iter__(self):
+               return iter(self._mailbox)
+
+       def __getitem__(self, n):
+               return self._mailbox[n]
+
+       def _fetch(self):
+               """This method will fetch messages from user's mailbox.
+               """
+               request = self._connection.get('conversations.json')
+
+               if request.status_code != 200:
+                       raise errors.DiaspyError('wrong status code: {0}'.format(request.status_code))
+               mailbox = request.json()
+               self._mailbox = [models.Conversation(self._connection, c['conversation']['id']) for c in mailbox]
index d28aa73542582d238ee225fa8a941d2c90def1a1..d5d3f0e6ab52df02fcd13f4968dca8efc896e6b6 100644 (file)
@@ -7,158 +7,158 @@ 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...
+       # 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
-    raised by diaspy.
-    """
-    pass
+       """Base exception for all errors
+       raised by diaspy.
+       """
+       pass
 
 
 class LoginError(DiaspyError):
-    """Exception raised when something
-    bad happens while performing actions
-    related to logging in.
-    """
-    pass
+       """Exception raised when something
+       bad happens while performing actions
+       related to logging in.
+       """
+       pass
 
 
 class TokenError(DiaspyError):
-    pass
+       pass
 
 class CSRFProtectionKickedIn(TokenError):
-    pass
+       pass
 
 
 class DataError(DiaspyError):
-    pass
+       pass
 
 
 class InvalidDataError(DataError):
-    pass
+       pass
 
 
 class KeyMissingFromFetchedData(InvalidDataError):
-    pass
+       pass
 
 
 class UserError(DiaspyError):
-    """Exception raised when something related to users goes wrong.
-    """
-    pass
+       """Exception raised when something related to users goes wrong.
+       """
+       pass
 
 
 class InvalidHandleError(DiaspyError):
-    """Raised when invalid handle is found.
-    """
-    pass
+       """Raised when invalid handle is found.
+       """
+       pass
 
 
 class SearchError(DiaspyError):
-    """Exception raised when something related to search goes wrong.
-    """
-    pass
+       """Exception raised when something related to search goes wrong.
+       """
+       pass
 
 
 class ConversationError(DiaspyError):
-    """Exception raised when something related to conversations goes wrong.
-    """
-    pass
+       """Exception raised when something related to conversations goes wrong.
+       """
+       pass
 
 
 class AspectError(DiaspyError):
-    """Exception raised when something related to aspects goes wrong.
-    """
-    pass
+       """Exception raised when something related to aspects goes wrong.
+       """
+       pass
 
 class UserIsNotMemberOfAspect(AspectError):
-    pass
+       pass
 
 
 class PostError(DiaspyError):
-    """Exception raised when something related to posts goes wrong.
-    """
-    pass
+       """Exception raised when something related to posts goes wrong.
+       """
+       pass
 
 
 class StreamError(DiaspyError):
-    """Exception raised when something related to streams goes wrong.
-    """
-    pass
+       """Exception raised when something related to streams goes wrong.
+       """
+       pass
 
 
 class SettingsError(DiaspyError):
-    """Exception raised when something related to settings goes wrong.
-    """
-    pass
+       """Exception raised when something related to settings goes wrong.
+       """
+       pass
 
 class SearchError(DiaspyError):
-    """Exception raised when something related to searching goes wrong.
-    """
-    pass
+       """Exception raised when something related to searching goes wrong.
+       """
+       pass
 
 class TagError(DiaspyError):
-    """Exception raised when something related to settings goes wrong.
-    """
-    pass
+       """Exception raised when something related to settings goes wrong.
+       """
+       pass
 
 def react(r, message='', accepted=[200, 201, 202, 203, 204, 205, 206], exception=DiaspyError):
-    """This method tries to decide how to react
-    to a response code passed to it. If it's an
-    error code it will raise an exception (it will
-    call `throw()` method.
-
-    If response code is not accepted AND cannot
-    be matched to any exception, generic exception
-    (DiaspyError) is raised (provided that `exception`
-    param was left untouched).
-
-    By default `accepted` param contains all HTTP
-    success codes.
-
-    User can force type of exception to raise by passing
-    `exception` param.
-
-    :param r: response code
-    :type r: int
-    :param message: message for the exception
-    :type message: str
-    :param accepted: list of accepted error codes
-    :type accepted: list
-    :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
-
-    if e is not None: e = exception
-    throw(e, message=message)
+       """This method tries to decide how to react
+       to a response code passed to it. If it's an
+       error code it will raise an exception (it will
+       call `throw()` method.
+
+       If response code is not accepted AND cannot
+       be matched to any exception, generic exception
+       (DiaspyError) is raised (provided that `exception`
+       param was left untouched).
+
+       By default `accepted` param contains all HTTP
+       success codes.
+
+       User can force type of exception to raise by passing
+       `exception` param.
+
+       :param r: response code
+       :type r: int
+       :param message: message for the exception
+       :type message: str
+       :param accepted: list of accepted error codes
+       :type accepted: list
+       :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
+
+       if e is not None: e = exception
+       throw(e, message=message)
 
 
 def throw(e, message=''):
-    """This function throws an error with given message.
-    If None is passed as `e` throw() will not raise
-    anything.
-
-    :param e: exception to throw
-    :type e: any valid exception type or None
-    :param message: message for exception
-    :type message: str
-    """
-    warnings.warn(DeprecationWarning)
-    if e is None: pass
-    else: raise e(message)
+       """This function throws an error with given message.
+       If None is passed as `e` throw() will not raise
+       anything.
+
+       :param e: exception to throw
+       :type e: any valid exception type or None
+       :param message: message for exception
+       :type message: str
+       """
+       warnings.warn(DeprecationWarning)
+       if e is None: pass
+       else: raise e(message)
index d6281ddb035a90378573352acd070dec1a55cb2a..fd2a6437665ad30f892a14e1fc2580ca8a690604 100644 (file)
@@ -12,607 +12,603 @@ import re
 from diaspy import errors
 
 class Aspect():
-    """This class represents an aspect.
-
-    Class can be initialized by passing either an id and/or name as
-    parameters.
-    If both are missing, an exception will be raised.
-    """
-    def __init__(self, connection, id, name=None):
-        self._connection = connection
-        self.id, self.name = id, name
-        self._cached = []
-
-    def getUsers(self, fetch = True):
-        """Returns list of GUIDs of users who are listed in this aspect.
-        """
-        if fetch:
-            request = self._connection.get('contacts.json?a_id={}'.format(self.id))
-            self._cached = request.json()
-        return self._cached
-
-    def removeAspect(self):
-        """
-        --> POST /aspects/{id} HTTP/1.1
-        --> _method=delete&authenticity_token={token}
-
-        <-- HTTP/1.1 302 Found
-
-        TODO: status_codes
-
-        Removes whole aspect.
-        :returns: None
-        """
-        request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format(self.id))
-
-        if request.status_code != 302:
-            raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
-    def addUser(self, user_id):
-        """Add user to current aspect.
-
-        :param user_id: user to add to aspect
-        :type user_id: int
-        :returns: JSON from request
-
-        --> POST /aspect_memberships HTTP/1.1
-        --> Accept: application/json, text/javascript, */*; q=0.01
-        --> Content-Type: application/json; charset=UTF-8
-
-        --> {"aspect_id":123,"person_id":123}
-
-        <-- HTTP/1.1 200 OK
-        """
-        data = {
-                'aspect_id': self.id,
-                'person_id': user_id
-                }
-        headers = {'content-type': 'application/json',
-                   'accept': 'application/json'
-                   }
-
-        request = self._connection.tokenFrom('contacts').post('aspect_memberships', data=json.dumps(data), headers=headers)
-
-        if request.status_code == 400:
-            raise errors.AspectError('duplicate record, user already exists in aspect: {0}'.format(request.status_code))
-        elif request.status_code == 404:
-            raise errors.AspectError('user not found from this pod: {0}'.format(request.status_code))
-        elif request.status_code != 200:
-            raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
-        response = None
-        try:
-            response = request.json()
-        except json.decoder.JSONDecodeError:
-            """ Should be OK now, but I'll leave this commentary here 
-            at first to see if anything comes up """
-            # FIXME For some (?) reason removing users from aspects works, but
-            # adding them is a no-go and Diaspora* kicks us out with CSRF errors.
-            # Weird.
-            pass
-
-        if response is None:
-            raise errors.CSRFProtectionKickedIn()
-
-        # Now you should fetchguid(fetch_stream=False) on User to update aspect membership_id's
-        # Or update it locally with the response
-        return response
-
-    def removeUser(self, user):
-        """Remove user from current aspect.
-
-        :param user: user to remove from aspect
-        :type user: diaspy.people.User object
-        """
-        membership_id = None
-        to_remove = None
-        for each in user.aspectMemberships():
-            print(self.id, each)
-            if each.get('aspect', {}).get('id') == self.id:
-                membership_id = each.get('id')
-                to_remove = each
-                break # no need to continue
-
-        if membership_id is None:
-            raise errors.UserIsNotMemberOfAspect(user, self)
-
-        request = self._connection.delete('aspect_memberships/{0}'.format(membership_id))
-
-        if request.status_code == 404:
-            raise errors.AspectError('cannot remove user from aspect, probably tried too fast after adding: {0}'.format(request.status_code))
-
-        elif request.status_code != 200:
-            raise errors.AspectError('cannot remove user from aspect: {0}'.format(request.status_code))
-
-        if 'contact' in user.data: # User object
-            if to_remove: user.data['contact']['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
-        else: # User object from Contacts()
-            if to_remove: user.data['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
-        return request.json()
+       """This class represents an aspect.
+
+       Class can be initialized by passing either an id and/or name as
+       parameters.
+       If both are missing, an exception will be raised.
+       """
+       def __init__(self, connection, id, name=None):
+               self._connection = connection
+               self.id, self.name = id, name
+               self._cached = []
+
+       def getUsers(self, fetch = True):
+               """Returns list of GUIDs of users who are listed in this aspect.
+               """
+               if fetch:
+                       request = self._connection.get('contacts.json?a_id={}'.format(self.id))
+                       self._cached = request.json()
+               return self._cached
+
+       def removeAspect(self):
+               """
+               --> POST /aspects/{id} HTTP/1.1
+               --> _method=delete&authenticity_token={token}
+
+               <-- HTTP/1.1 302 Found
+
+               TODO: status_codes
+
+               Removes whole aspect.
+               :returns: None
+               """
+               request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format(self.id))
+
+               if request.status_code != 302:
+                       raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+       def addUser(self, user_id):
+               """Add user to current aspect.
+
+               :param user_id: user to add to aspect
+               :type user_id: int
+               :returns: JSON from request
+
+               --> POST /aspect_memberships HTTP/1.1
+               --> Accept: application/json, text/javascript, */*; q=0.01
+               --> Content-Type: application/json; charset=UTF-8
+
+               --> {"aspect_id":123,"person_id":123}
+
+               <-- HTTP/1.1 200 OK
+               """
+               data = {'aspect_id': self.id,
+                               'person_id': user_id}
+               headers = {'content-type': 'application/json',
+                                       'accept': 'application/json'}
+               request = self._connection.tokenFrom('contacts').post('aspect_memberships', data=json.dumps(data), headers=headers)
+
+               if request.status_code == 400:
+                       raise errors.AspectError('duplicate record, user already exists in aspect: {0}'.format(request.status_code))
+               elif request.status_code == 404:
+                       raise errors.AspectError('user not found from this pod: {0}'.format(request.status_code))
+               elif request.status_code != 200:
+                       raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+               response = None
+               try:
+                       response = request.json()
+               except json.decoder.JSONDecodeError:
+                       """ Should be OK now, but I'll leave this commentary here 
+                       at first to see if anything comes up """
+                       # FIXME For some (?) reason removing users from aspects works, but
+                       # adding them is a no-go and Diaspora* kicks us out with CSRF errors.
+                       # Weird.
+                       pass
+
+               if response is None:
+                       raise errors.CSRFProtectionKickedIn()
+
+               # Now you should fetchguid(fetch_stream=False) on User to update aspect membership_id's
+               # Or update it locally with the response
+               return response
+
+       def removeUser(self, user):
+               """Remove user from current aspect.
+
+               :param user: user to remove from aspect
+               :type user: diaspy.people.User object
+               """
+               membership_id = None
+               to_remove = None
+               for each in user.aspectMemberships():
+                       print(self.id, each)
+                       if each.get('aspect', {}).get('id') == self.id:
+                               membership_id = each.get('id')
+                               to_remove = each
+                               break # no need to continue
+
+               if membership_id is None:
+                       raise errors.UserIsNotMemberOfAspect(user, self)
+
+               request = self._connection.delete('aspect_memberships/{0}'.format(membership_id))
+
+               if request.status_code == 404:
+                       raise errors.AspectError('cannot remove user from aspect, probably tried too fast after adding: {0}'.format(request.status_code))
+
+               elif request.status_code != 200:
+                       raise errors.AspectError('cannot remove user from aspect: {0}'.format(request.status_code))
+
+               if 'contact' in user.data: # User object
+                       if to_remove: user.data['contact']['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
+               else: # User object from Contacts()
+                       if to_remove: user.data['aspect_memberships'].remove( to_remove ) # remove local aspect membership_id
+               return request.json()
 
 
 class Notification():
-    """This class represents single notification.
-    """
-    _who_regexp = re.compile(r'/people/([0-9a-f]+)["\']{1} class=["\']{1}hovercardable')
-    _when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
-    _aboutid_regexp = re.compile(r'/posts/[0-9a-f]+')
-    _htmltag_regexp = re.compile('</?[a-z]+( *[a-z_-]+=["\'].*?["\'])* */?>')
-
-    def __init__(self, connection, data):
-        self._connection = connection
-        self.type = data['type']
-        self._data = data[self.type]
-        self.id = self._data['id']
-        self.unread = self._data['unread']
-
-    def __getitem__(self, key):
-        """Returns a key from notification data.
-        """
-        return self._data[key]
-
-    def __str__(self):
-        """Returns notification note.
-        """
-        string = re.sub(self._htmltag_regexp, '', self._data['note_html'])
-        string = string.strip().split('\n')[0]
-        while '  ' in string: string = string.replace('  ', ' ')
-        return string
-
-    def __repr__(self):
-        """Returns notification note with more details.
-        """
-        return '{0}: {1}'.format(self.when(), str(self))
-
-    def about(self):
-        """Returns id of post about which the notification is informing OR:
-        If the id is None it means that it's about user so .who() is called.
-        """
-        about = self._aboutid_regexp.search(self._data['note_html'])
-        if about is None: about = self.who()
-        else: about = int(about.group(0)[7:])
-        return about
-
-    def who(self):
-        """Returns list of guids of the users who caused you to get the notification.
-        """
-        return [who for who in self._who_regexp.findall(self._data['note_html'])]
-
-    def when(self):
-        """Returns UTC time as found in note_html.
-        """
-        return self._data['created_at']
-
-    def mark(self, unread=False):
-        """Marks notification to read/unread.
-        Marks notification to read if `unread` is False.
-        Marks notification to unread if `unread` is True.
-
-        :param unread: which state set for notification
-        :type unread: bool
-        """
-        headers = {'x-csrf-token': repr(self._connection)}
-        params = {'set_unread': json.dumps(unread)}
-        self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
-        self._data['unread'] = unread
+       """This class represents single notification.
+       """
+       _who_regexp = re.compile(r'/people/([0-9a-f]+)["\']{1} class=["\']{1}hovercardable')
+       _when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
+       _aboutid_regexp = re.compile(r'/posts/[0-9a-f]+')
+       _htmltag_regexp = re.compile('</?[a-z]+( *[a-z_-]+=["\'].*?["\'])* */?>')
+
+       def __init__(self, connection, data):
+               self._connection = connection
+               self.type = data['type']
+               self._data = data[self.type]
+               self.id = self._data['id']
+               self.unread = self._data['unread']
+
+       def __getitem__(self, key):
+               """Returns a key from notification data.
+               """
+               return self._data[key]
+
+       def __str__(self):
+               """Returns notification note.
+               """
+               string = re.sub(self._htmltag_regexp, '', self._data['note_html'])
+               string = string.strip().split('\n')[0]
+               while '  ' in string: string = string.replace('  ', ' ')
+               return string
+
+       def __repr__(self):
+               """Returns notification note with more details.
+               """
+               return '{0}: {1}'.format(self.when(), str(self))
+
+       def about(self):
+               """Returns id of post about which the notification is informing OR:
+               If the id is None it means that it's about user so .who() is called.
+               """
+               about = self._aboutid_regexp.search(self._data['note_html'])
+               if about is None: about = self.who()
+               else: about = int(about.group(0)[7:])
+               return about
+
+       def who(self):
+               """Returns list of guids of the users who caused you to get the notification.
+               """
+               return [who for who in self._who_regexp.findall(self._data['note_html'])]
+
+       def when(self):
+               """Returns UTC time as found in note_html.
+               """
+               return self._data['created_at']
+
+       def mark(self, unread=False):
+               """Marks notification to read/unread.
+               Marks notification to read if `unread` is False.
+               Marks notification to unread if `unread` is True.
+
+               :param unread: which state set for notification
+               :type unread: bool
+               """
+               headers = {'x-csrf-token': repr(self._connection)}
+               params = {'set_unread': json.dumps(unread)}
+               self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
+               self._data['unread'] = unread
 
 
 class Conversation():
-    """This class represents a conversation.
-
-    .. note::
-        Remember that you need to have access to the conversation.
-    """
-    def __init__(self, connection, id, fetch=True):
-        """
-        :param conv_id: id of the post and not the guid!
-        :type conv_id: str
-        :param connection: connection object used to authenticate
-        :type connection: connection.Connection
-        """
-        self._connection = connection
-        self.id = id
-        self._data = {}
-        if fetch: self._fetch()
-
-    def _fetch(self):
-        """Fetches JSON data representing conversation.
-        """
-        request = self._connection.get('conversations/{}.json'.format(self.id))
-        if request.status_code == 200:
-            self._data = request.json()['conversation']
-        else:
-            raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
-
-    def answer(self, text):
-        """Answer that conversation
-
-        :param text: text to answer.
-        :type text: str
-        """
-        data = {'message[text]': text,
-                'utf8': '&#x2713;',
-                'authenticity_token': repr(self._connection)}
-
-        request = self._connection.post('conversations/{}/messages'.format(self.id),
-                                        data=data,
-                                        headers={'accept': 'application/json'})
-        if request.status_code != 200:
-            raise errors.ConversationError('{0}: Answer could not be posted.'
-                                           .format(request.status_code))
-        return request.json()
-
-    def delete(self):
-        """Delete this conversation.
-        Has to be implemented.
-        """
-        data = {'authenticity_token': repr(self._connection)}
-
-        request = self._connection.delete('conversations/{0}/visibility/'
-                                          .format(self.id),
-                                          data=data,
-                                          headers={'accept': 'application/json'})
-
-        if request.status_code != 404:
-            raise errors.ConversationError('{0}: Conversation could not be deleted.'
-                                           .format(request.status_code))
-
-    def get_subject(self):
-        """Returns the subject of this conversation
-        """
-        return self._data['subject']
+       """This class represents a conversation.
+
+       .. note::
+               Remember that you need to have access to the conversation.
+       """
+       def __init__(self, connection, id, fetch=True):
+               """
+               :param conv_id: id of the post and not the guid!
+               :type conv_id: str
+               :param connection: connection object used to authenticate
+               :type connection: connection.Connection
+               """
+               self._connection = connection
+               self.id = id
+               self._data = {}
+               if fetch: self._fetch()
+
+       def _fetch(self):
+               """Fetches JSON data representing conversation.
+               """
+               request = self._connection.get('conversations/{}.json'.format(self.id))
+               if request.status_code == 200:
+                       self._data = request.json()['conversation']
+               else:
+                       raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
+
+       def answer(self, text):
+               """Answer that conversation
+
+               :param text: text to answer.
+               :type text: str
+               """
+               data = {'message[text]': text,
+                               'utf8': '&#x2713;',
+                               'authenticity_token': repr(self._connection)}
+
+               request = self._connection.post('conversations/{}/messages'.format(self.id),
+                                                                               data=data,
+                                                                               headers={'accept': 'application/json'})
+               if request.status_code != 200:
+                       raise errors.ConversationError('{0}: Answer could not be posted.'
+                                                                                  .format(request.status_code))
+               return request.json()
+
+       def delete(self):
+               """Delete this conversation.
+               Has to be implemented.
+               """
+               data = {'authenticity_token': repr(self._connection)}
+
+               request = self._connection.delete('conversations/{0}/visibility/'
+                                                               .format(self.id),
+                                                               data=data,
+                                                               headers={'accept': 'application/json'})
+
+               if request.status_code != 404:
+                       raise errors.ConversationError('{0}: Conversation could not be deleted.'
+                                                                               .format(request.status_code))
+
+       def get_subject(self):
+               """Returns the subject of this conversation
+               """
+               return self._data['subject']
 
 
 class Comment():
-    """Represents comment on post.
-
-    Does not require Connection() object. Note that you should not manually
-    create `Comment()` objects -- they are designed to be created automatically
-    by `Post()` objects.
-    """
-    def __init__(self, data):
-        self._data = data
-        self.id = data['id']
-        self.guid = data['guid']
-
-    def __str__(self):
-        """Returns comment's text.
-        """
-        return self._data['text']
-
-    def __repr__(self):
-        """Returns comments text and author.
-        Format: AUTHOR (AUTHOR'S GUID): COMMENT
-        """
-        return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
-
-    def when(self):
-        """Returns time when the comment had been created.
-        """
-        return self._data['created_at']
-
-    def author(self, key='name'):
-        """Returns author of the comment.
-        """
-        return self._data['author'][key]
+       """Represents comment on post.
+
+       Does not require Connection() object. Note that you should not manually
+       create `Comment()` objects -- they are designed to be created automatically
+       by `Post()` objects.
+       """
+       def __init__(self, data):
+               self._data = data
+               self.id = data['id']
+               self.guid = data['guid']
+
+       def __str__(self):
+               """Returns comment's text.
+               """
+               return self._data['text']
+
+       def __repr__(self):
+               """Returns comments text and author.
+               Format: AUTHOR (AUTHOR'S GUID): COMMENT
+               """
+               return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
+
+       def when(self):
+               """Returns time when the comment had been created.
+               """
+               return self._data['created_at']
+
+       def author(self, key='name'):
+               """Returns author of the comment.
+               """
+               return self._data['author'][key]
 
 class Comments():
-    def __init__(self, comments=None):
-        self._comments = comments
-
-    def __iter__(self):
-        if self._comments:
-            for comment in self._comments:
-                yield comment
-
-    def __len__(self):
-        if self._comments:
-            return len(self._comments)
-
-    def __getitem__(self, index):
-        if self._comments:
-            return self._comments[index]
-
-    def __bool__(self):
-        if self._comments:
-            return True
-        return False
-
-    def ids(self):
-        return [c.id for c in self._comments]
-
-    def add(self, comment):
-        """ Expects comment object
-        TODO self._comments is None sometimes, have to look into it."""
-        if comment and self._comments:
-            self._comments.append(comment)
-
-    def set(self, comments):
-        """Sets comments wich already have a Comment obj"""
-        if comments:
-            self._comments = comments
-
-    def set_json(self, json_comments):
-        """Sets comments for this post from post data."""
-        if json_comments:
-            self._comments = [Comment(c) for c in json_comments]
+       def __init__(self, comments=None):
+               self._comments = comments
+
+       def __iter__(self):
+               if self._comments:
+                       for comment in self._comments:
+                               yield comment
+
+       def __len__(self):
+               if self._comments:
+                       return len(self._comments)
+
+       def __getitem__(self, index):
+               if self._comments:
+                       return self._comments[index]
+
+       def __bool__(self):
+               if self._comments:
+                       return True
+               return False
+
+       def ids(self):
+               return [c.id for c in self._comments]
+
+       def add(self, comment):
+               """ Expects comment object
+               TODO self._comments is None sometimes, have to look into it."""
+               if comment and self._comments:
+                       self._comments.append(comment)
+
+       def set(self, comments):
+               """Sets comments wich already have a Comment obj"""
+               if comments:
+                       self._comments = comments
+
+       def set_json(self, json_comments):
+               """Sets comments for this post from post data."""
+               if json_comments:
+                       self._comments = [Comment(c) for c in json_comments]
 
 class Post():
-    """This class represents a post.
-
-    .. note::
-        Remember that you need to have access to the post.
-    """
-    def __init__(self, connection, id=0, guid='', fetch=True, comments=True, post_data=None):
-        """
-        :param id: id of the post (GUID is recommended)
-        :type id: int
-        :param guid: GUID of the post
-        :type guid: str
-        :param connection: connection object used to authenticate
-        :type connection: connection.Connection
-        :param fetch: defines whether to fetch post's data or not
-        :type fetch: bool
-        :param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
-        :type comments: bool
-        :param post_data: contains post data so no need to fetch the post if this is set, until you want to update post data
-        :type: json
-        """
-        if not (guid or id): raise TypeError('neither guid nor id was provided')
-        self._connection = connection
-        self.id = id
-        self.guid = guid
-        self._data = {}
-        self.comments = Comments()
-        if post_data:
-            self._data = post_data
-
-        if 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['interactions']['comments'] )
-
-    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'])
-
-    def __str__(self):
-        """Returns text of a post.
-        """
-        return self._data['text']
-
-    def __getitem__(self, key):
-        """FIXME This is deprecated, use diaspy.models.Post.data() instead to access
-        data of Post objects.
-        """
-        return self._data[key]
-
-    def __dict__(self):
-        """Returns dictionary of posts data.
-        FIXME This is deprecated, use diaspy.models.Post.data() instead.
-        """
-        return self._data
-
-    def _fetchdata(self):
-        """This function retrieves data of the post.
-
-        :returns: guid of post whose data was fetched
-        """
-        if self.id: id = self.id
-        if self.guid: id = self.guid
-        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()
-        return self['guid']
-
-    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.
-        """
-        id = self._data['id']
-        if self['interactions']['comments_count']:
-            request = self._connection.get('posts/{0}/comments.json'.format(id))
-            if request.status_code != 200:
-                raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
-            else:
-                self.comments.set([Comment(c) for c in request.json()])
-
-    def update(self):
-        """Updates post data.
-        FIXME This is deprecated.
-        """
-        print('diaspy.models.Post.update() is deprecated. Use diaspy.models.Post.update() instead.')
-        self._fetchdata()
-        self._fetchcomments()
-
-    def fetch(self, comments = False):
-        """Fetches post data.
-        Use this function instead of diaspy.models.Post.update().
-        """
-        self._fetchdata()
-        if comments:
-            self._fetchcomments()
-        return self
-
-    def data(self, data = None):
-        if data is not None:
-            self._data = data
-        return self._data
-
-    def like(self):
-        """This function likes a post.
-        It abstracts the 'Like' functionality.
-
-        :returns: dict -- json formatted like object.
-        """
-        data = {'authenticity_token': repr(self._connection)}
-
-        request = self._connection.post('posts/{0}/likes'.format(self.id),    
-                                        data=data,
-                                        headers={'accept': 'application/json'})
-
-        if request.status_code != 201:
-            raise errors.PostError('{0}: Post could not be liked.'
-                                   .format(request.status_code))
-
-        likes_json = request.json()
-        if likes_json:
-            self._data['interactions']['likes'] = [likes_json]
-        return likes_json
-
-    def reshare(self):
-        """This function reshares a post
-        """
-        data = {'root_guid': self._data['guid'],
-                'authenticity_token': repr(self._connection)}
-
-        request = self._connection.post('reshares',
-                                        data=data,
-                                        headers={'accept': 'application/json'})
-        if request.status_code != 201:
-            raise Exception('{0}: Post could not be reshared'.format(request.status_code))
-        return request.json()
-
-    def comment(self, text):
-        """This function comments on a post
-
-        :param text: text to comment.
-        :type text: str
-        """
-        data = {'text': text,
-                'authenticity_token': repr(self._connection)}
-        request = self._connection.post('posts/{0}/comments'.format(self.id),
-                                        data=data,
-                                        headers={'accept': 'application/json'})
-
-        if request.status_code != 201:
-            raise Exception('{0}: Comment could not be posted.'
-                            .format(request.status_code))
-        return Comment(request.json())
-
-    def vote_poll(self, poll_answer_id):
-        """This function votes on a post's poll
-
-        :param poll_answer_id: id to poll vote.
-        :type poll_answer_id: int
-        """
-        poll_id = self._data['poll']['poll_id']
-        data = {'poll_answer_id': poll_answer_id,
-                'poll_id': poll_id,
-                'post_id': self.id,
-                'authenticity_token': repr(self._connection)}
-        request = self._connection.post('posts/{0}/poll_participations'.format(self.id),
-                                        data=data,
-                                        headers={'accept': 'application/json'})
-        if request.status_code != 201:
-            raise Exception('{0}: Vote on poll failed.'
-                            .format(request.status_code))
-        return request.json()
-
-    def hide(self):
-        """
-        ->    PUT /share_visibilities/42 HTTP/1.1
-              post_id=123
-        <-    HTTP/1.1 200 OK
-        """
-        headers = {'x-csrf-token': repr(self._connection)}
-        params = {'post_id': json.dumps(self.id)}
-        request = self._connection.put('share_visibilities/42', params=params, headers=headers)
-        if request.status_code != 200:
-            raise Exception('{0}: Failed to hide post.'
-                            .format(request.status_code))
-
-    def mute(self):
-        """
-        ->    POST /blocks HTTP/1.1
-            {"block":{"person_id":123}}
-        <-    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'] } })
-        request = self._connection.post('blocks', data=data, headers=headers)
-        if request.status_code != 204:
-            raise Exception('{0}: Failed to block person'
-                            .format(request.status_code))
-
-    def subscribe(self):
-        """
-        ->    POST /posts/123/participation HTTP/1.1
-        <-    HTTP/1.1 201 Created
-        """
-        headers = {'x-csrf-token': repr(self._connection)}
-        data = {}
-        request = self._connection.post('posts/{}/participation'
-                            .format( self.id ), data=data, headers=headers)
-        if request.status_code != 201:
-            raise Exception('{0}: Failed to subscribe to post'
-                            .format(request.status_code))
-
-    def unsubscribe(self):
-        """
-        ->    POST /posts/123/participation HTTP/1.1
-              _method=delete
-        <-    HTTP/1.1 200 OK
-        """
-        headers = {'x-csrf-token': repr(self._connection)}
-        data = { "_method": "delete" }
-        request = self._connection.post('posts/{}/participation'
-                            .format( self.id ), headers=headers, data=data)
-        if request.status_code != 200:
-            raise Exception('{0}: Failed to unsubscribe to post'
-                            .format(request.status_code))
-
-    def report(self):
-        """
-        TODO
-        """
-        pass
-
-    def delete(self):
-        """ This function deletes this post
-        """
-        data = {'authenticity_token': repr(self._connection)}
-        request = self._connection.delete('posts/{0}'.format(self.id),
-                                          data=data,
-                                          headers={'accept': 'application/json'})
-        if request.status_code != 204:
-            raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
-
-    def delete_comment(self, comment_id):
-        """This function removes a comment from a post
-
-        :param comment_id: id of the comment to remove.
-        :type comment_id: str
-        """
-        data = {'authenticity_token': repr(self._connection)}
-        request = self._connection.delete('posts/{0}/comments/{1}'
-                                          .format(self.id, comment_id),
-                                          data=data,
-                                          headers={'accept': 'application/json'})
-
-        if request.status_code != 204:
-            raise errors.PostError('{0}: Comment could not be deleted'
-                                   .format(request.status_code))
-
-    def delete_like(self):
-        """This function removes a like from a post
-        """
-        data = {'authenticity_token': repr(self._connection)}
-        url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
-        request = self._connection.delete(url, data=data)
-        if request.status_code != 204:
-            raise errors.PostError('{0}: Like could not be removed.'
-                                   .format(request.status_code))
-
-    def author(self, key='name'):
-        """Returns author of the post.
-        :param key: all keys available in data['author']
-        """
-        return self._data['author'][key]
+       """This class represents a post.
+
+       .. note::
+               Remember that you need to have access to the post.
+       """
+       def __init__(self, connection, id=0, guid='', fetch=True, comments=True, post_data=None):
+               """
+               :param id: id of the post (GUID is recommended)
+               :type id: int
+               :param guid: GUID of the post
+               :type guid: str
+               :param connection: connection object used to authenticate
+               :type connection: connection.Connection
+               :param fetch: defines whether to fetch post's data or not
+               :type fetch: bool
+               :param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
+               :type comments: bool
+               :param post_data: contains post data so no need to fetch the post if this is set, until you want to update post data
+               :type: json
+               """
+               if not (guid or id): raise TypeError('neither guid nor id was provided')
+               self._connection = connection
+               self.id = id
+               self.guid = guid
+               self._data = {}
+               self.comments = Comments()
+               if post_data:
+                       self._data = post_data
+
+               if 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['interactions']['comments'] )
+
+       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'])
+
+       def __str__(self):
+               """Returns text of a post.
+               """
+               return self._data['text']
+
+       def __getitem__(self, key):
+               """FIXME This is deprecated, use diaspy.models.Post.data() instead to access
+               data of Post objects.
+               """
+               return self._data[key]
+
+       def __dict__(self):
+               """Returns dictionary of posts data.
+               FIXME This is deprecated, use diaspy.models.Post.data() instead.
+               """
+               return self._data
+
+       def _fetchdata(self):
+               """This function retrieves data of the post.
+
+               :returns: guid of post whose data was fetched
+               """
+               if self.id: id = self.id
+               if self.guid: id = self.guid
+               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()
+               return self['guid']
+
+       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.
+               """
+               id = self._data['id']
+               if self['interactions']['comments_count']:
+                       request = self._connection.get('posts/{0}/comments.json'.format(id))
+                       if request.status_code != 200:
+                               raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
+                       else:
+                               self.comments.set([Comment(c) for c in request.json()])
+
+       def update(self):
+               """Updates post data.
+               FIXME This is deprecated.
+               """
+               print('diaspy.models.Post.update() is deprecated. Use diaspy.models.Post.update() instead.')
+               self._fetchdata()
+               self._fetchcomments()
+
+       def fetch(self, comments = False):
+               """Fetches post data.
+               Use this function instead of diaspy.models.Post.update().
+               """
+               self._fetchdata()
+               if comments:
+                       self._fetchcomments()
+               return self
+
+       def data(self, data = None):
+               if data is not None:
+                       self._data = data
+               return self._data
+
+       def like(self):
+               """This function likes a post.
+               It abstracts the 'Like' functionality.
+
+               :returns: dict -- json formatted like object.
+               """
+               data = {'authenticity_token': repr(self._connection)}
+
+               request = self._connection.post('posts/{0}/likes'.format(self.id),    
+                                                                               data=data,
+                                                                               headers={'accept': 'application/json'})
+
+               if request.status_code != 201:
+                       raise errors.PostError('{0}: Post could not be liked.'
+                                                                  .format(request.status_code))
+
+               likes_json = request.json()
+               if likes_json:
+                       self._data['interactions']['likes'] = [likes_json]
+               return likes_json
+
+       def reshare(self):
+               """This function reshares a post
+               """
+               data = {'root_guid': self._data['guid'],
+                               'authenticity_token': repr(self._connection)}
+
+               request = self._connection.post('reshares',
+                                                                               data=data,
+                                                                               headers={'accept': 'application/json'})
+               if request.status_code != 201:
+                       raise Exception('{0}: Post could not be reshared'.format(request.status_code))
+               return request.json()
+
+       def comment(self, text):
+               """This function comments on a post
+
+               :param text: text to comment.
+               :type text: str
+               """
+               data = {'text': text,
+                               'authenticity_token': repr(self._connection)}
+               request = self._connection.post('posts/{0}/comments'.format(self.id),
+                                                                               data=data,
+                                                                               headers={'accept': 'application/json'})
+
+               if request.status_code != 201:
+                       raise Exception('{0}: Comment could not be posted.'
+                                                       .format(request.status_code))
+               return Comment(request.json())
+
+       def vote_poll(self, poll_answer_id):
+               """This function votes on a post's poll
+
+               :param poll_answer_id: id to poll vote.
+               :type poll_answer_id: int
+               """
+               poll_id = self._data['poll']['poll_id']
+               data = {'poll_answer_id': poll_answer_id,
+                               'poll_id': poll_id,
+                               'post_id': self.id,
+                               'authenticity_token': repr(self._connection)}
+               request = self._connection.post('posts/{0}/poll_participations'.format(self.id),
+                                                                               data=data,
+                                                                               headers={'accept': 'application/json'})
+               if request.status_code != 201:
+                       raise Exception('{0}: Vote on poll failed.'
+                                                       .format(request.status_code))
+               return request.json()
+
+       def hide(self):
+               """
+               ->    PUT /share_visibilities/42 HTTP/1.1
+                         post_id=123
+               <-    HTTP/1.1 200 OK
+               """
+               headers = {'x-csrf-token': repr(self._connection)}
+               params = {'post_id': json.dumps(self.id)}
+               request = self._connection.put('share_visibilities/42', params=params, headers=headers)
+               if request.status_code != 200:
+                       raise Exception('{0}: Failed to hide post.'
+                                                       .format(request.status_code))
+
+       def mute(self):
+               """
+               ->    POST /blocks HTTP/1.1
+                       {"block":{"person_id":123}}
+               <-    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'] } })
+               request = self._connection.post('blocks', data=data, headers=headers)
+               if request.status_code != 204:
+                       raise Exception('{0}: Failed to block person'
+                                                       .format(request.status_code))
+
+       def subscribe(self):
+               """
+               ->    POST /posts/123/participation HTTP/1.1
+               <-    HTTP/1.1 201 Created
+               """
+               headers = {'x-csrf-token': repr(self._connection)}
+               data = {}
+               request = self._connection.post('posts/{}/participation'
+                                                       .format( self.id ), data=data, headers=headers)
+               if request.status_code != 201:
+                       raise Exception('{0}: Failed to subscribe to post'
+                                                       .format(request.status_code))
+
+       def unsubscribe(self):
+               """
+               ->    POST /posts/123/participation HTTP/1.1
+                         _method=delete
+               <-    HTTP/1.1 200 OK
+               """
+               headers = {'x-csrf-token': repr(self._connection)}
+               data = { "_method": "delete" }
+               request = self._connection.post('posts/{}/participation'
+                                                       .format( self.id ), headers=headers, data=data)
+               if request.status_code != 200:
+                       raise Exception('{0}: Failed to unsubscribe to post'
+                                                       .format(request.status_code))
+
+       def report(self):
+               """
+               TODO
+               """
+               pass
+
+       def delete(self):
+               """ This function deletes this post
+               """
+               data = {'authenticity_token': repr(self._connection)}
+               request = self._connection.delete('posts/{0}'.format(self.id),
+                                                                                 data=data,
+                                                                                 headers={'accept': 'application/json'})
+               if request.status_code != 204:
+                       raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
+
+       def delete_comment(self, comment_id):
+               """This function removes a comment from a post
+
+               :param comment_id: id of the comment to remove.
+               :type comment_id: str
+               """
+               data = {'authenticity_token': repr(self._connection)}
+               request = self._connection.delete('posts/{0}/comments/{1}'
+                                                                                 .format(self.id, comment_id),
+                                                                                 data=data,
+                                                                                 headers={'accept': 'application/json'})
+
+               if request.status_code != 204:
+                       raise errors.PostError('{0}: Comment could not be deleted'
+                                                                  .format(request.status_code))
+
+       def delete_like(self):
+               """This function removes a like from a post
+               """
+               data = {'authenticity_token': repr(self._connection)}
+               url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
+               request = self._connection.delete(url, data=data)
+               if request.status_code != 204:
+                       raise errors.PostError('{0}: Like could not be removed.'
+                                                                  .format(request.status_code))
+
+       def author(self, key='name'):
+               """Returns author of the post.
+               :param key: all keys available in data['author']
+               """
+               return self._data['author'][key]
index 1fbd44f98c5e9b3e808a594dee0667fc9dec710c..cb5b42f3ff25f984022eaca700cffec47bc93a44 100644 (file)
@@ -11,90 +11,90 @@ from diaspy.models import Notification
 
 
 class Notifications():
-    """This class represents notifications of a user.
-    """
-    def __init__(self, connection):
-        self._connection = connection
-        self._data = {}
-        self._notifications = self.get()
-        self.page = 1
-
-    def __iter__(self):
-        return iter(self._notifications)
-
-    def __getitem__(self, n):
-        return self._notifications[n]
-
-    def _finalise(self, notifications):
-        self._data['unread_count'] = notifications['unread_count']
-        self._data['unread_count_by_type'] = notifications['unread_count_by_type']
-        return [Notification(self._connection, n) for n in notifications.get('notification_list', [])]
-
-    def last(self):
-        """Returns list of most recent notifications.
-        """
-        params = {'per_page': 5, '_': int(round(time.time(), 3)*1000)}
-        headers = {'x-csrf-token': repr(self._connection)}
-
-        request = self._connection.get('notifications.json', headers=headers, params=params)
-
-        if request.status_code != 200:
-            raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
-        return self._finalise(request.json())
-
-    def _expand(self, new_notifications):
-        ids = [notification.id for notification in self._notifications]
-        notifications = self._notifications
-        data = self._data
-        for n in new_notifications:
-            if n.id not in ids:
-                if n.unread:
-                    data['unread_count'] +=1
-                    data['unread_count_by_type'][n.type] +=1
-                notifications.append(n)
-                ids.append(n.id)
-        self._notifications = notifications
-        self._data = data
-
-    def _update(self, new_notifications):
-        ids = [notification.id for notification in self._notifications]
-        notifications = self._notifications
-        data = self._data
-
-        update = False
-        if new_notifications[len(new_notifications)-1].id not in ids:
-            update = True
-
-        for i in range(len(new_notifications)):
-            if new_notifications[-i].id not in ids:
-                if new_notifications[-i].unread:
-                    data['unread_count'] +=1
-                    data['unread_count_by_type'][new_notifications[-i].type] +=1
-                notifications = [new_notifications[-i]] + notifications
-                ids.append(new_notifications[-i].id)
-        self._notifications = notifications
-        self._data = data
-        if update: self.update() # if there is a gap
-
-    def update(self, per_page=5, page=1):
-        result = self.get(per_page=per_page, page=page)
-        if result: self._update( result )
-
-    def more(self, per_page=5, page=0):
-        if not page: page = self.page + 1
-        self.page = page
-        result = self.get(per_page=per_page, page=page)
-        if result:
-            self._expand( result )
-
-    def get(self, per_page=5, page=1):
-        """Returns list of notifications.
-        """
-        params = {'per_page': per_page, 'page': page}
-        headers = {'x-csrf-token': repr(self._connection)}
-
-        request = self._connection.get('notifications.json', headers=headers, params=params)
-
-        if request.status_code != 200:
-            raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
-        return self._finalise(request.json())
+       """This class represents notifications of a user.
+       """
+       def __init__(self, connection):
+               self._connection = connection
+               self._data = {}
+               self._notifications = self.get()
+               self.page = 1
+
+       def __iter__(self):
+               return iter(self._notifications)
+
+       def __getitem__(self, n):
+               return self._notifications[n]
+
+       def _finalise(self, notifications):
+               self._data['unread_count'] = notifications['unread_count']
+               self._data['unread_count_by_type'] = notifications['unread_count_by_type']
+               return [Notification(self._connection, n) for n in notifications.get('notification_list', [])]
+
+       def last(self):
+               """Returns list of most recent notifications.
+               """
+               params = {'per_page': 5, '_': int(round(time.time(), 3)*1000)}
+               headers = {'x-csrf-token': repr(self._connection)}
+
+               request = self._connection.get('notifications.json', headers=headers, params=params)
+
+               if request.status_code != 200:
+                       raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
+               return self._finalise(request.json())
+
+       def _expand(self, new_notifications):
+               ids = [notification.id for notification in self._notifications]
+               notifications = self._notifications
+               data = self._data
+               for n in new_notifications:
+                       if n.id not in ids:
+                               if n.unread:
+                                       data['unread_count'] +=1
+                                       data['unread_count_by_type'][n.type] +=1
+                               notifications.append(n)
+                               ids.append(n.id)
+               self._notifications = notifications
+               self._data = data
+
+       def _update(self, new_notifications):
+               ids = [notification.id for notification in self._notifications]
+               notifications = self._notifications
+               data = self._data
+
+               update = False
+               if new_notifications[len(new_notifications)-1].id not in ids:
+                       update = True
+
+               for i in range(len(new_notifications)):
+                       if new_notifications[-i].id not in ids:
+                               if new_notifications[-i].unread:
+                                       data['unread_count'] +=1
+                                       data['unread_count_by_type'][new_notifications[-i].type] +=1
+                               notifications = [new_notifications[-i]] + notifications
+                               ids.append(new_notifications[-i].id)
+               self._notifications = notifications
+               self._data = data
+               if update: self.update() # if there is a gap
+
+       def update(self, per_page=5, page=1):
+               result = self.get(per_page=per_page, page=page)
+               if result: self._update( result )
+
+       def more(self, per_page=5, page=0):
+               if not page: page = self.page + 1
+               self.page = page
+               result = self.get(per_page=per_page, page=page)
+               if result:
+                       self._expand( result )
+
+       def get(self, per_page=5, page=1):
+               """Returns list of notifications.
+               """
+               params = {'per_page': per_page, 'page': page}
+               headers = {'x-csrf-token': repr(self._connection)}
+
+               request = self._connection.get('notifications.json', headers=headers, params=params)
+
+               if request.status_code != 200:
+                       raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
+               return self._finalise(request.json())
index 5a88386a7771c9c7c5cc14730e097256cd5d12c1..14a869a3676ee2b0817b35b3c4c855aef40d0c00 100644 (file)
@@ -12,393 +12,393 @@ from diaspy import search
 
 
 def sephandle(handle):
-    """Separate Diaspora* handle into pod pod and user.
+       """Separate Diaspora* handle into pod pod and user.
 
-    :returns: two-tuple (pod, user)
-    """
-    if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
-        raise errors.InvalidHandleError('{0}'.format(handle))
-    handle = handle.split('@')
-    pod, user = handle[1], handle[0]
-    return (pod, user)
+       :returns: two-tuple (pod, user)
+       """
+       if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
+               raise errors.InvalidHandleError('{0}'.format(handle))
+       handle = handle.split('@')
+       pod, user = handle[1], handle[0]
+       return (pod, user)
 
 
 class User():
-    """This class abstracts a D* user.
-    This object goes around the limitations of current D* API and will
-    extract user data using black magic.
-    However, no chickens are harmed when you use it.
-
-    The parameter fetch should be either 'posts', 'data' or 'none'. By
-    default it is 'posts' which means in addition to user data, stream
-    will be fetched. If user has not posted yet diaspy will not be able
-    to extract the information from his/her posts. Since there is no official
-    way to do it we rely on user posts. If this will be the case user
-    will be notified with appropriate exception message.
-
-    If fetch is 'data', only user data will be fetched. If the user is
-    not found, no exception will be returned.
-
-    When creating new User() one can pass either guid, handle and/or id as
-    optional parameters. GUID takes precedence over handle when fetching
-    user stream. When fetching user data, handle is required.
-    """
-    @classmethod
-    def parse(cls, connection, data):
-        person = data.get('person')
-        if person is None:
-            raise errors.KeyMissingFromFetchedData('person', data)
-
-        guid = person.get('guid')
-        if guid is None:
-            raise errors.KeyMissingFromFetchedData('guid', person)
-
-        handle = person.get('diaspora_id')
-        if handle is None:
-            raise errors.KeyMissingFromFetchedData('diaspora_id', person)
-
-        person_id = person.get('id')
-        if person_id is None:
-            raise errors.KeyMissingFromFetchedData('id', person)
-
-        return User(connection, guid, handle, id, data=data)
-
-    def __init__(self, connection, guid='', handle='', fetch='posts', id=0, data=None):
-        self._connection = connection
-        self.stream = []
-        self.data = {
-            'guid': guid,
-            'handle': handle,
-            'id': id,
-        }
-        self.photos = []
-        if data: self.data.update( data )
-        if fetch: self._fetch(fetch)
-
-    def __getitem__(self, key):
-        return self.data[key]
-
-    def __str__(self):
-        return self.data.get('guid', '<guid missing>')
-
-    def __repr__(self):
-        return '{0} ({1})'.format(self.handle(), self.guid())
-
-    def handle(self):
-        if 'handle' in self.data: return self['handle']
-        return self.data.get('diaspora_id', 'Unknown handle')
-
-    def guid(self):
-        return self.data.get('guid', '<guid missing>')
-
-    def id(self):
-        return self.data['id']
-
-    def _fetchstream(self):
-        self.stream = Outer(self._connection, guid=self['guid'])
-
-    def _fetch(self, fetch):
-        """Fetch user posts or data.
-        """
-        if fetch == 'posts':
-            if self.handle() and not self['guid']: self.fetchhandle()
-            else: self.fetchguid()
-        elif fetch == 'data' and self['handle']:
-            self.fetchprofile()
-
-    def _finalize_data(self, data):
-        """Adjustments are needed to have similar results returned
-        by search feature and fetchguid()/fetchhandle().
-        """
-        return data
-
-    def _postproc(self, request):
-        """Makes necessary modifications to user data and
-        sets up a stream.
-
-        :param request: request object
-        :type request: request
-        """
-        if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
-        data = request.json()
-        self.data = self._finalize_data(data)
-
-    def fetchhandle(self, protocol='https'):
-        """Fetch user data and posts using Diaspora handle.
-        """
-        pod, user = sephandle(self['handle'])
-        request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
-        self._postproc(request)
-        self._fetchstream()
-
-    def fetchguid(self, fetch_stream=True):
-        """Fetch user data and posts (if fetch_stream is True) using guid.
-        """
-        if self['guid']:
-            request = self._connection.get('people/{0}.json'.format(self['guid']))
-            self._postproc(request)
-            if fetch_stream: self._fetchstream()
-        else:
-            raise errors.UserError('GUID not set')
-
-    def fetchprofile(self):
-        """Fetches user data.
-        """ 
-        data = search.Search(self._connection).user(self.handle())
-        if not data:
-            raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self.handle(), self._connection.pod))
-        else:
-            self.data.update( data[0] )
-
-    def aspectMemberships(self):
-        if 'contact' in self.data:
-            return self.data.get('contact', {}).get('aspect_memberships', [])
-        else:
-            return self.data.get('aspect_memberships', [])
-
-    def getPhotos(self):
-        """
-        --> GET /people/{GUID}/photos.json HTTP/1.1
-
-        <-- HTTP/1.1 200 OK
-
-        {
-            "photos":[
-                {
-                    "id":{photo_id},
-                    "guid":"{photo_guid}",
-                    "created_at":"2018-03-08T23:48:31.000Z",
-                    "author":{
-                        "id":{author_id},
-                        "guid":"{author_guid}",
-                        "name":"{author_name}",
-                        "diaspora_id":"{diaspora_id}",
-                        "avatar":{"small":"{avatar_url_small}","medium":"{avatar_url_medium}","large":"{avatar_url_large}"}
-                    },
-                    "sizes":{
-                        "small":"{photo_url}",
-                        "medium":"{photo_url}",
-                        "large":"{photo_url}"
-                    },
-                    "dimensions":{"height":847,"width":998},
-                    "status_message":{
-                        "id":{post_id}
-                    }
-                },{ ..
-        }
-
-        if there are no photo's it returns:
-        {"photos":[]}
-        """
-
-        request = self._connection.get('/people/{0}/photos.json'.format(self['guid']))
-        if request.status_code != 200: raise errors.UserError('could not fetch photos for user: {0}'.format(self['guid']))
-
-        json = request.json()
-        if json: self.photos = json['photos']
-        return json['photos']
-
-    def getHCard(self):
-        """Returns json containing user HCard.
-        --> /people/{guid}/hovercard.json?_={timestamp}
-
-        <-- HTTP/2.0 200 OK
-        {
-            "id":123,
-            "guid":"1234567890abcdef",
-            "name":"test",
-            "diaspora_id":"batman@test.test",
-            "contact":false,
-            "profile":{
-                "avatar":"https://nicetesturl.url/image.jpg",
-                "tags":["tag1", "tag2", "tag3", "tag4", "tag5"]}
-        }
-        """
-        timestamp = int(time.mktime(time.gmtime()))
-        request = self._connection.get('/people/{0}/hovercard.json?_={}'.format(self['guid'], timestamp))
-        if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
-        return request.json()
-
-    def deletePhoto(self, photo_id):
-        """
-        --> DELETE /photos/{PHOTO_ID} HTTP/1.1
-        <-- HTTP/1.1 204 No Content
-        """
-        request = self._connection.delete('/photos/{0}'.format(photo_id))
-        if request.status_code != 204: raise errors.UserError('could not delete photo_id: {0}'.format(photo_id))
+       """This class abstracts a D* user.
+       This object goes around the limitations of current D* API and will
+       extract user data using black magic.
+       However, no chickens are harmed when you use it.
+
+       The parameter fetch should be either 'posts', 'data' or 'none'. By
+       default it is 'posts' which means in addition to user data, stream
+       will be fetched. If user has not posted yet diaspy will not be able
+       to extract the information from his/her posts. Since there is no official
+       way to do it we rely on user posts. If this will be the case user
+       will be notified with appropriate exception message.
+
+       If fetch is 'data', only user data will be fetched. If the user is
+       not found, no exception will be returned.
+
+       When creating new User() one can pass either guid, handle and/or id as
+       optional parameters. GUID takes precedence over handle when fetching
+       user stream. When fetching user data, handle is required.
+       """
+       @classmethod
+       def parse(cls, connection, data):
+               person = data.get('person')
+               if person is None:
+                       raise errors.KeyMissingFromFetchedData('person', data)
+
+               guid = person.get('guid')
+               if guid is None:
+                       raise errors.KeyMissingFromFetchedData('guid', person)
+
+               handle = person.get('diaspora_id')
+               if handle is None:
+                       raise errors.KeyMissingFromFetchedData('diaspora_id', person)
+
+               person_id = person.get('id')
+               if person_id is None:
+                       raise errors.KeyMissingFromFetchedData('id', person)
+
+               return User(connection, guid, handle, id, data=data)
+
+       def __init__(self, connection, guid='', handle='', fetch='posts', id=0, data=None):
+               self._connection = connection
+               self.stream = []
+               self.data = {
+                       'guid': guid,
+                       'handle': handle,
+                       'id': id,
+               }
+               self.photos = []
+               if data: self.data.update( data )
+               if fetch: self._fetch(fetch)
+
+       def __getitem__(self, key):
+               return self.data[key]
+
+       def __str__(self):
+               return self.data.get('guid', '<guid missing>')
+
+       def __repr__(self):
+               return '{0} ({1})'.format(self.handle(), self.guid())
+
+       def handle(self):
+               if 'handle' in self.data: return self['handle']
+               return self.data.get('diaspora_id', 'Unknown handle')
+
+       def guid(self):
+               return self.data.get('guid', '<guid missing>')
+
+       def id(self):
+               return self.data['id']
+
+       def _fetchstream(self):
+               self.stream = Outer(self._connection, guid=self['guid'])
+
+       def _fetch(self, fetch):
+               """Fetch user posts or data.
+               """
+               if fetch == 'posts':
+                       if self.handle() and not self['guid']: self.fetchhandle()
+                       else: self.fetchguid()
+               elif fetch == 'data' and self['handle']:
+                       self.fetchprofile()
+
+       def _finalize_data(self, data):
+               """Adjustments are needed to have similar results returned
+               by search feature and fetchguid()/fetchhandle().
+               """
+               return data
+
+       def _postproc(self, request):
+               """Makes necessary modifications to user data and
+               sets up a stream.
+
+               :param request: request object
+               :type request: request
+               """
+               if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
+               data = request.json()
+               self.data = self._finalize_data(data)
+
+       def fetchhandle(self, protocol='https'):
+               """Fetch user data and posts using Diaspora handle.
+               """
+               pod, user = sephandle(self['handle'])
+               request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
+               self._postproc(request)
+               self._fetchstream()
+
+       def fetchguid(self, fetch_stream=True):
+               """Fetch user data and posts (if fetch_stream is True) using guid.
+               """
+               if self['guid']:
+                       request = self._connection.get('people/{0}.json'.format(self['guid']))
+                       self._postproc(request)
+                       if fetch_stream: self._fetchstream()
+               else:
+                       raise errors.UserError('GUID not set')
+
+       def fetchprofile(self):
+               """Fetches user data.
+               """ 
+               data = search.Search(self._connection).user(self.handle())
+               if not data:
+                       raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self.handle(), self._connection.pod))
+               else:
+                       self.data.update( data[0] )
+
+       def aspectMemberships(self):
+               if 'contact' in self.data:
+                       return self.data.get('contact', {}).get('aspect_memberships', [])
+               else:
+                       return self.data.get('aspect_memberships', [])
+
+       def getPhotos(self):
+               """
+               --> GET /people/{GUID}/photos.json HTTP/1.1
+
+               <-- HTTP/1.1 200 OK
+
+               {
+                       "photos":[
+                               {
+                                       "id":{photo_id},
+                                       "guid":"{photo_guid}",
+                                       "created_at":"2018-03-08T23:48:31.000Z",
+                                       "author":{
+                                               "id":{author_id},
+                                               "guid":"{author_guid}",
+                                               "name":"{author_name}",
+                                               "diaspora_id":"{diaspora_id}",
+                                               "avatar":{"small":"{avatar_url_small}","medium":"{avatar_url_medium}","large":"{avatar_url_large}"}
+                                       },
+                                       "sizes":{
+                                               "small":"{photo_url}",
+                                               "medium":"{photo_url}",
+                                               "large":"{photo_url}"
+                                       },
+                                       "dimensions":{"height":847,"width":998},
+                                       "status_message":{
+                                               "id":{post_id}
+                                       }
+                               },{ ..
+               }
+
+               if there are no photo's it returns:
+               {"photos":[]}
+               """
+
+               request = self._connection.get('/people/{0}/photos.json'.format(self['guid']))
+               if request.status_code != 200: raise errors.UserError('could not fetch photos for user: {0}'.format(self['guid']))
+
+               json = request.json()
+               if json: self.photos = json['photos']
+               return json['photos']
+
+       def getHCard(self):
+               """Returns json containing user HCard.
+               --> /people/{guid}/hovercard.json?_={timestamp}
+
+               <-- HTTP/2.0 200 OK
+               {
+                       "id":123,
+                       "guid":"1234567890abcdef",
+                       "name":"test",
+                       "diaspora_id":"batman@test.test",
+                       "contact":false,
+                       "profile":{
+                               "avatar":"https://nicetesturl.url/image.jpg",
+                               "tags":["tag1", "tag2", "tag3", "tag4", "tag5"]}
+               }
+               """
+               timestamp = int(time.mktime(time.gmtime()))
+               request = self._connection.get('/people/{0}/hovercard.json?_={}'.format(self['guid'], timestamp))
+               if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
+               return request.json()
+
+       def deletePhoto(self, photo_id):
+               """
+               --> DELETE /photos/{PHOTO_ID} HTTP/1.1
+               <-- HTTP/1.1 204 No Content
+               """
+               request = self._connection.delete('/photos/{0}'.format(photo_id))
+               if request.status_code != 204: raise errors.UserError('could not delete photo_id: {0}'.format(photo_id))
 
 class Me():
-    """Object represetnting current user.
-    """
-    _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
-    _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
+       """Object represetnting current user.
+       """
+       _userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
+       _userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
 
-    def __init__(self, connection):
-        self._connection = connection
+       def __init__(self, connection):
+               self._connection = connection
 
-    def getInfo(self):
-        """This function returns the current user's attributes.
+       def getInfo(self):
+               """This function returns the current user's attributes.
 
-        :returns: dict
-        """
-        request = self._connection.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)
+               :returns: dict
+               """
+               request = self._connection.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)
 
 
 class Contacts():
-    """This class represents user's list of contacts.
-    """
-    def __init__(self, connection, fetch=False, set=''):
-        self._connection = connection
-        self.contacts = None
-        if fetch: self.contacts = self.get(set)
-
-    def __getitem__(self, index):
-        return self.contacts[index]
-
-    def addAspect(self, name, visible=False):
-        """
-        --> POST /aspects HTTP/1.1
-        --> {"person_id":null,"name":"test","contacts_visible":false}
-
-        <-- HTTP/1.1 200 OK
-
-        Add new aspect.
-
-        TODO: status_code's
-
-        :param name: aspect name to add
-        :type name: str
-        :param visible: sets if contacts in aspect are visible for each and other
-        :type visible: bool
-        :returns: JSON from request
-        """
-        data = {
-            'person_id': None,
-            'name': name,
-            'contacts_visible': visible
-        }
-        headers={'content-type': 'application/json',
-                 'accept': 'application/json' }
-        request = self._connection.tokenFrom('contacts').post('aspects', headers=headers, data=json.dumps(data))
-
-        if request.status_code == 400:
-            raise errors.AspectError('duplicate record, aspect alreadt exists: {0}'.format(request.status_code))
-        elif request.status_code != 200:
-            raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
-        new_aspect = request.json()
-        self._connection.userdata()['aspects'].append( new_aspect )
-
-        return new_aspect
-
-    def deleteAspect(self, aspect_id):
-        """
-        --> POST /aspects/{ASPECT_ID} HTTP/1.1
-            _method=delete&authenticity_token={TOKEN}
-            Content-Type: application/x-www-form-urlencoded
-
-        <-- HTTP/1.1 302 Found
-            Content-Type: text/html; charset=utf-8
-        """
-        request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format( aspect_id ))
-
-        if request.status_code != 200: # since we don't post but delete
-            raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
-
-    def add(self, user_id, aspect_ids):
-        """Add user to aspects of given ids.
-
-        :param user_id: user id (not guid)
-        :type user_id: str
-        :param aspect_ids: list of aspect ids
-        :type aspect_ids: list
-        """
-        # TODO update self.contacts
-        # Returns {"aspect_id":123,"person_id":123}
-        for aid in aspect_ids:
-            new_aspect_membership = Aspect(self._connection, aid).addUser(user_id)
-
-            # user.
-            if new_aspect_membership:
-                for user in self.contacts:
-                    if int(user.data['person_id']) == int(user_id):
-                        user.data['aspect_memberships'].append( new_aspect_membership )
-                        return new_aspect_membership
-
-    def remove(self, user_id, aspect_ids):
-        """Remove user from aspects of given ids.
-
-        :param user_id: user id
-        :type user_id: str
-        :param aspect_ids: list of aspect ids
-        :type aspect_ids: list
-        """
-        for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
-
-    def get(self, set='', page=0):
-        """Returns list of user contacts.
-        Contact is a User() who is in one or more of user's
-        aspects.
-
-        By default, it will return list of users who are in
-        user's aspects.
-
-        If `set` is `all` it will also include users who only share
-        with logged user and are not in his/hers aspects.
-
-        If `set` is `only_sharing` it will return users who are only
-        sharing with logged user and ARE NOT in his/hers aspects.
-
-        # On "All contacts" button diaspora
-        on the time of testing this I had 20 contacts and 10 that 
-        where only sharing with me. So 30 in total.
-
-        -->    GET /contacts?set=all HTTP/1.1
-            <-- HTTP/1.1 200 OK
-            returned 25 contacts (5 only sharing with me)
-
-        -->    GET /contacts.json?page=1&set=all&_=1524410225376 HTTP/1.1
-            <-- HTTP/1.1 200 OK
-            returned the same list as before.
-
-        --> GET /contacts.json?page=2&set=all&_=1524410225377 HTTP/1.1
-            <-- HTTP/1.1 200 OK
-            returned the other 5 that where only sharing with me.
-
-        --> GET /contacts.json?page=3&set=all&_=1524410225378 HTTP/1.1
-            <-- HTTP/1.1 200 OK
-            returned empty list.
-
-        It appears that /contacts?set=all returns a maximum of 25 
-        contacts.
-
-        So if /contacts?set=all returns 25 contacts then request next 
-        page until page returns a list with less then 25. I don't see a 
-        reason why we should request page=1 'cause the previous request 
-        will be the same. So begin with page=2 if /contacts?set=all 
-        returns 25.
-
-        :param set: if passed could be 'all' or 'only_sharing'
-        :type set: str
-        """
-        params = {}
-        if set:
-            params['set'] = set
-            params['_'] = int(time.mktime(time.gmtime()))
-            if page: params['page'] = page
-
-        request = self._connection.get('contacts.json', params=params)
-        if request.status_code != 200:
-            raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
-
-        json = request.json()
-        users = [User.parse(self._connection, each) for each in json]
-        if len(json) == 25:
-            if not page: page = 1
-            users += self.get(set=set, page=page+1)
-        return users
+       """This class represents user's list of contacts.
+       """
+       def __init__(self, connection, fetch=False, set=''):
+               self._connection = connection
+               self.contacts = None
+               if fetch: self.contacts = self.get(set)
+
+       def __getitem__(self, index):
+               return self.contacts[index]
+
+       def addAspect(self, name, visible=False):
+               """
+               --> POST /aspects HTTP/1.1
+               --> {"person_id":null,"name":"test","contacts_visible":false}
+
+               <-- HTTP/1.1 200 OK
+
+               Add new aspect.
+
+               TODO: status_code's
+
+               :param name: aspect name to add
+               :type name: str
+               :param visible: sets if contacts in aspect are visible for each and other
+               :type visible: bool
+               :returns: JSON from request
+               """
+               data = {
+                       'person_id': None,
+                       'name': name,
+                       'contacts_visible': visible
+               }
+               headers={'content-type': 'application/json',
+                                'accept': 'application/json' }
+               request = self._connection.tokenFrom('contacts').post('aspects', headers=headers, data=json.dumps(data))
+
+               if request.status_code == 400:
+                       raise errors.AspectError('duplicate record, aspect alreadt exists: {0}'.format(request.status_code))
+               elif request.status_code != 200:
+                       raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+               new_aspect = request.json()
+               self._connection.userdata()['aspects'].append( new_aspect )
+
+               return new_aspect
+
+       def deleteAspect(self, aspect_id):
+               """
+               --> POST /aspects/{ASPECT_ID} HTTP/1.1
+                       _method=delete&authenticity_token={TOKEN}
+                       Content-Type: application/x-www-form-urlencoded
+
+               <-- HTTP/1.1 302 Found
+                       Content-Type: text/html; charset=utf-8
+               """
+               request = self._connection.tokenFrom('contacts').delete('aspects/{}'.format( aspect_id ))
+
+               if request.status_code != 200: # since we don't post but delete
+                       raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
+
+       def add(self, user_id, aspect_ids):
+               """Add user to aspects of given ids.
+
+               :param user_id: user id (not guid)
+               :type user_id: str
+               :param aspect_ids: list of aspect ids
+               :type aspect_ids: list
+               """
+               # TODO update self.contacts
+               # Returns {"aspect_id":123,"person_id":123}
+               for aid in aspect_ids:
+                       new_aspect_membership = Aspect(self._connection, aid).addUser(user_id)
+
+                       # user.
+                       if new_aspect_membership:
+                               for user in self.contacts:
+                                       if int(user.data['person_id']) == int(user_id):
+                                               user.data['aspect_memberships'].append( new_aspect_membership )
+                                               return new_aspect_membership
+
+       def remove(self, user_id, aspect_ids):
+               """Remove user from aspects of given ids.
+
+               :param user_id: user id
+               :type user_id: str
+               :param aspect_ids: list of aspect ids
+               :type aspect_ids: list
+               """
+               for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
+
+       def get(self, set='', page=0):
+               """Returns list of user contacts.
+               Contact is a User() who is in one or more of user's
+               aspects.
+
+               By default, it will return list of users who are in
+               user's aspects.
+
+               If `set` is `all` it will also include users who only share
+               with logged user and are not in his/hers aspects.
+
+               If `set` is `only_sharing` it will return users who are only
+               sharing with logged user and ARE NOT in his/hers aspects.
+
+               # On "All contacts" button diaspora
+               on the time of testing this I had 20 contacts and 10 that 
+               where only sharing with me. So 30 in total.
+
+               -->    GET /contacts?set=all HTTP/1.1
+                       <-- HTTP/1.1 200 OK
+                       returned 25 contacts (5 only sharing with me)
+
+               -->    GET /contacts.json?page=1&set=all&_=1524410225376 HTTP/1.1
+                       <-- HTTP/1.1 200 OK
+                       returned the same list as before.
+
+               --> GET /contacts.json?page=2&set=all&_=1524410225377 HTTP/1.1
+                       <-- HTTP/1.1 200 OK
+                       returned the other 5 that where only sharing with me.
+
+               --> GET /contacts.json?page=3&set=all&_=1524410225378 HTTP/1.1
+                       <-- HTTP/1.1 200 OK
+                       returned empty list.
+
+               It appears that /contacts?set=all returns a maximum of 25 
+               contacts.
+
+               So if /contacts?set=all returns 25 contacts then request next 
+               page until page returns a list with less then 25. I don't see a 
+               reason why we should request page=1 'cause the previous request 
+               will be the same. So begin with page=2 if /contacts?set=all 
+               returns 25.
+
+               :param set: if passed could be 'all' or 'only_sharing'
+               :type set: str
+               """
+               params = {}
+               if set:
+                       params['set'] = set
+                       params['_'] = int(time.mktime(time.gmtime()))
+                       if page: params['page'] = page
+
+               request = self._connection.get('contacts.json', params=params)
+               if request.status_code != 200:
+                       raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
+
+               json = request.json()
+               users = [User.parse(self._connection, each) for each in json]
+               if len(json) == 25:
+                       if not page: page = 1
+                       users += self.get(set=set, page=page+1)
+               return users
index 11f77c735052de799a389d72cd2bc6266148ff93..513917531e11a934ac9636e311697de3e0964c58 100644 (file)
@@ -8,41 +8,41 @@ from diaspy import errors
 
 
 class Search():
-    """This object is used for searching for content on Diaspora*.
-    """
-    def __init__(self, connection):
-        self._connection = connection
-
-    def lookupUser(self, handle):
-        """This function will launch a webfinger lookup from the pod for the
-        handle requested. Response code is returned and if the lookup was successful,
-        user should soon be searchable via pod used for connection.
-
-        :param string: Handle to search for.
-        """
-        request = self._connection.get('people', headers={'accept': 'text/html'}, params={'q': handle})
-        return request.status_code
-
-    def user(self, query):
-        """Searches for a user.
-        Will return list of dictionaries containing
-        data of found users.
-        """
-        request = self._connection.get('people.json', params={'q': query, 'utf-8': '%u2713'})
-        if request.status_code != 200:
-            raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
-        return request.json()
-
-    def tags(self, query, limit=10):
-        """Retrieve tag suggestions.
-
-        :param query: query used to search
-        :type query: str
-        :param limit: maxmal number of suggestions returned
-        :type limit: int
-        """
-        params = {'q': query, 'limit': limit}
-        request = self._connection.get('tags', params=params, headers={'x-csrf-token': repr(self._connection)})
-        if request.status_code != 200:
-            raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
-        return [i['name'] for i in request.json()]
+       """This object is used for searching for content on Diaspora*.
+       """
+       def __init__(self, connection):
+               self._connection = connection
+
+       def lookupUser(self, handle):
+               """This function will launch a webfinger lookup from the pod for the
+               handle requested. Response code is returned and if the lookup was successful,
+               user should soon be searchable via pod used for connection.
+
+               :param string: Handle to search for.
+               """
+               request = self._connection.get('people', headers={'accept': 'text/html'}, params={'q': handle})
+               return request.status_code
+
+       def user(self, query):
+               """Searches for a user.
+               Will return list of dictionaries containing
+               data of found users.
+               """
+               request = self._connection.get('people.json', params={'q': query, 'utf-8': '%u2713'})
+               if request.status_code != 200:
+                       raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
+               return request.json()
+
+       def tags(self, query, limit=10):
+               """Retrieve tag suggestions.
+
+               :param query: query used to search
+               :type query: str
+               :param limit: maxmal number of suggestions returned
+               :type limit: int
+               """
+               params = {'q': query, 'limit': limit}
+               request = self._connection.get('tags', params=params, headers={'x-csrf-token': repr(self._connection)})
+               if request.status_code != 200:
+                       raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
+               return [i['name'] for i in request.json()]
index c926efdfa01bd21781ee04769f61b8243e934887..5d1cdf45694bb2a53722cd91edcc1d9b75c02952 100644 (file)
@@ -14,296 +14,296 @@ from diaspy import errors, streams
 
 
 class Account():
-    """Provides interface to account settings.
-    """
-    email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"')
-    language_option_regexp = re.compile('<option value="([_a-zA-Z-]+)"(?: selected="selected")?>(.*?)</option>')
-
-    def __init__(self, connection):
-        self._connection = connection
-
-    def downloadxml(self):
-        """Returns downloaded XML.
-        """
-        request = self._connection.get('user/export')
-        return request.text
-
-    def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
-        """Downloads photos into the current working directory.
-        Sizes are: large, medium, small.
-        Filename is: {post_guid}_{photo_guid}.{extension}
-
-        Normally, this method will catch urllib-generated errors and
-        just issue warnings about photos that couldn't be downloaded.
-        However, with _critical param set to True errors will become
-        critical - the will be reraised in finally block.
-
-        :param size: size of the photos to download - large, medium or small
-        :type size: str
-        :param path: path to download (defaults to current working directory
-        :type path: str
-        :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
-        :type mark_nsfw: bool
-        :param _stream: diaspy.streams.Generic-like object (only for testing)
-        :param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
-
-        :returns: integer, number of photos downloaded
-        """
-        photos = 0
-        if _stream is None:
-            stream = streams.Activity(self._connection)
-            stream.full()
-        else:
-            stream = _stream
-        for i, post in enumerate(stream):
-            if post['nsfw'] is not False: nsfw = '-nsfw'
-            else: nsfw = ''
-            if post['photos']:
-                for n, photo in enumerate(post['photos']):
-                    # photo format -- .jpg, .png etc.
-                    ext = photo['sizes'][size].split('.')[-1]
-                    name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
-                    filename = os.path.join(path, name)
-                    try:
-                        urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
-                    except (urllib.error.HTTPError, urllib.error.URLError) as e:
-                        warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
-                    finally:
-                        if _critical: raise
-                photos += 1
-        return photos
-
-    def setEmail(self, email):
-        """Changes user's email.
-        """
-        data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
-        request = self._connection.post('user', data=data, allow_redirects=False)
-        if request.status_code != 302:
-            raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
-
-    def getEmail(self):
-        """Returns currently used email.
-        """
-        data = self._connection.get('user/edit')
-        email = self.email_regexp.search(data.text)
-        if email is None: email = ''
-        else: email = email.group(1)
-        return email
-
-    def setLanguage(self, lang):
-        """Changes user's email.
-
-        :param lang: language identifier from getLanguages()
-        """
-        data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
-        request = self._connection.post('user', data=data, allow_redirects=False)
-        if request.status_code != 302:
-            raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
-
-    def getLanguages(self):
-        """Returns a list of tuples containing ('Language name', 'identifier').
-        One of the Black Magic(tm) methods.
-        """
-        request = self._connection.get('user/edit')
-        return self.language_option_regexp.findall(request.text)
+       """Provides interface to account settings.
+       """
+       email_regexp = re.compile('<input id="user_email" name="user\[email\]" size="30" type="text" value="(.+?)"')
+       language_option_regexp = re.compile('<option value="([_a-zA-Z-]+)"(?: selected="selected")?>(.*?)</option>')
+
+       def __init__(self, connection):
+               self._connection = connection
+
+       def downloadxml(self):
+               """Returns downloaded XML.
+               """
+               request = self._connection.get('user/export')
+               return request.text
+
+       def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
+               """Downloads photos into the current working directory.
+               Sizes are: large, medium, small.
+               Filename is: {post_guid}_{photo_guid}.{extension}
+
+               Normally, this method will catch urllib-generated errors and
+               just issue warnings about photos that couldn't be downloaded.
+               However, with _critical param set to True errors will become
+               critical - the will be reraised in finally block.
+
+               :param size: size of the photos to download - large, medium or small
+               :type size: str
+               :param path: path to download (defaults to current working directory
+               :type path: str
+               :param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
+               :type mark_nsfw: bool
+               :param _stream: diaspy.streams.Generic-like object (only for testing)
+               :param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
+
+               :returns: integer, number of photos downloaded
+               """
+               photos = 0
+               if _stream is None:
+                       stream = streams.Activity(self._connection)
+                       stream.full()
+               else:
+                       stream = _stream
+               for i, post in enumerate(stream):
+                       if post['nsfw'] is not False: nsfw = '-nsfw'
+                       else: nsfw = ''
+                       if post['photos']:
+                               for n, photo in enumerate(post['photos']):
+                                       # photo format -- .jpg, .png etc.
+                                       ext = photo['sizes'][size].split('.')[-1]
+                                       name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
+                                       filename = os.path.join(path, name)
+                                       try:
+                                               urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
+                                       except (urllib.error.HTTPError, urllib.error.URLError) as e:
+                                               warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
+                                       finally:
+                                               if _critical: raise
+                               photos += 1
+               return photos
+
+       def setEmail(self, email):
+               """Changes user's email.
+               """
+               data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
+               request = self._connection.post('user', data=data, allow_redirects=False)
+               if request.status_code != 302:
+                       raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
+
+       def getEmail(self):
+               """Returns currently used email.
+               """
+               data = self._connection.get('user/edit')
+               email = self.email_regexp.search(data.text)
+               if email is None: email = ''
+               else: email = email.group(1)
+               return email
+
+       def setLanguage(self, lang):
+               """Changes user's email.
+
+               :param lang: language identifier from getLanguages()
+               """
+               data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
+               request = self._connection.post('user', data=data, allow_redirects=False)
+               if request.status_code != 302:
+                       raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
+
+       def getLanguages(self):
+               """Returns a list of tuples containing ('Language name', 'identifier').
+               One of the Black Magic(tm) methods.
+               """
+               request = self._connection.get('user/edit')
+               return self.language_option_regexp.findall(request.text)
 
 
 class Privacy():
-    """Provides interface to provacy settings.
-    """
-    def __init__(self, connection):
-        self._connection = connection
+       """Provides interface to provacy settings.
+       """
+       def __init__(self, connection):
+               self._connection = connection
 
 
 class Profile():
-    """Provides interface to profile settigns.
-
-    WARNING:
-
-        Because of the way update requests for profile are created every field must be sent.
-        The `load()` method is used to load all information into the dictionary.
-        Setters can then be used to adjust the data.
-        Finally, `update()` can be called to send data back to pod.
-    """
-    firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
-    lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
-    bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>')
-    location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
-    gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
-    birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>')
-    birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>')
-    birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>')
-    is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
-    is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
-
-    def __init__(self, connection, no_load=False):
-        self._connection = connection
-        self.data = {'utf-8': '✓',
-                     '_method': 'put',
-                     'profile[first_name]': '',
-                     'profile[last_name]': '',
-                     'profile[tag_string]': '',
-                     'tags': '',
-                     'file': '',
-                     'profile[bio]': '',
-                     'profile[location]': '',
-                     'profile[gender]': '',
-                     'profile[date][year]': '',
-                     'profile[date][month]': '',
-                     'profile[date][day]': '',
-                     }
-        self._html = self._fetchhtml()
-        self._loaded = False
-        if not no_load: self.load()
-
-    def _fetchhtml(self):
-        """Fetches html that will be used to extract data.
-        """
-        return self._connection.get('profile/edit').text
-
-    def getName(self):
-        """Returns two-tuple: (first, last) name.
-        """
-        first = self.firstname_regexp.search(self._html).group(1)
-        last = self.lastname_regexp.search(self._html).group(1)
-        return (first, last)
-
-    def getTags(self):
-        """Returns tags user had selected when describing him/her-self.
-        """
-        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)]
-
-    def getBio(self):
-        """Returns user bio.
-        """
-        bio = self.bio_regexp.search(self._html).group(1)
-        return bio
-
-    def getLocation(self):
-        """Returns location string.
-        """
-        location = self.location_regexp.search(self._html).group(1)
-        return location
-
-    def getGender(self):
-        """Returns location string.
-        """
-        gender = self.gender_regexp.search(self._html).group(1)
-        return gender
-
-    def getBirthDate(self, named_month=False):
-        """Returns three-tuple: (year, month, day).
-
-        :param named_month: if True, return name of the month instead of integer
-        :type named_month: bool
-        """
-        year = self.birth_year_regexp.search(self._html)
-        if year is None: year = -1
-        else: year = int(year.group(1))
-        month = self.birth_month_regexp.search(self._html)
-        if month is None:
-            if named_month: month = ''
-            else: month = -1
-        else:
-            if named_month:
-                month = month.group(2)
-            else:
-                month = int(month.group(1))
-        day = self.birth_day_regexp.search(self._html)
-        if day is None: day = -1
-        else: day = int(day.group(1))
-        return (year, month, day)
-
-    def isSearchable(self):
-        """Returns True if profile is searchable.
-        """
-        searchable = self.is_searchable_regexp.search(self._html)
-        # this is because value="true" in every case so we just
-        # check if the field is "checked"
-        if searchable is None: searchable = False  # if it isn't - the regexp just won't match
-        else: searchable = True
-        return searchable
-
-    def isNSFW(self):
-        """Returns True if profile is marked as NSFW.
-        """
-        nsfw = self.is_nsfw_regexp.search(self._html)
-        if nsfw is None: nsfw = False
-        else: nsfw = True
-        return nsfw
-
-    def setName(self, first, last):
-        """Set first and last name.
-        """
-        self.data['profile[first_name]'] = first
-        self.data['profile[last_name]'] = last
-
-    def setTags(self, tags):
-        """Sets tags that describe the user.
-        """
-        self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
-
-    def setBio(self, bio):
-        """Set bio of a user.
-        """
-        self.data['profile[bio]'] = bio
-
-    def setLocation(self, location):
-        """Set location of a user.
-        """
-        self.data['profile[location]'] = location
-
-    def setGender(self, gender):
-        """Set gender of a user.
-        """
-        self.data['profile[gender]'] = gender
-
-    def setBirthDate(self, year, month, day):
-        """Set birth date of a user.
-        """
-        self.data['profile[date][year]'] = year
-        self.data['profile[date][month]'] = month
-        self.data['profile[date][day]'] = day
-
-    def setSearchable(self, searchable):
-        """Set user's searchable status.
-        """
-        self.data['profile[searchable]'] = json.dumps(searchable)
-
-    def setNSFW(self, nsfw):
-        """Set user NSFW status.
-        """
-        self.data['profile[nsfw]'] = json.dumps(nsfw)
-
-    def load(self):
-        """Loads profile data into self.data dictionary.
-        **Notice:** Not all keys are loaded yet.
-        """
-        self.setName(*self.getName())
-        self.setBio(self.getBio())
-        self.setLocation(self.getLocation())
-        self.setGender(self.getGender())
-        self.setBirthDate(*self.getBirthDate(named_month=False))
-        self.setSearchable(self.isSearchable())
-        self.setNSFW(self.isNSFW())
-        self.setTags(self.getTags())
-        self._loaded = True
-
-    def update(self):
-        """Updates profile information.
-        """
-        if not self._loaded: raise errors.DiaspyError('profile was not loaded')
-        self.data['authenticity_token'] = repr(self._connection)
-        print(self.data)
-        request = self._connection.post('profile', data=self.data, allow_redirects=False)
-        return request.status_code
+       """Provides interface to profile settigns.
+
+       WARNING:
+
+               Because of the way update requests for profile are created every field must be sent.
+               The `load()` method is used to load all information into the dictionary.
+               Setters can then be used to adjust the data.
+               Finally, `update()` can be called to send data back to pod.
+       """
+       firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
+       lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
+       bio_regexp = re.compile('<textarea id="profile_bio" name="profile\[bio\]" placeholder="Fill me out" rows="5">\n(.*?)</textarea>')
+       location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
+       gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
+       birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}</option>')
+       birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)</option>')
+       birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}</option>')
+       is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
+       is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
+
+       def __init__(self, connection, no_load=False):
+               self._connection = connection
+               self.data = {'utf-8': '✓',
+                                       '_method': 'put',
+                                       'profile[first_name]': '',
+                                       'profile[last_name]': '',
+                                       'profile[tag_string]': '',
+                                       'tags': '',
+                                       'file': '',
+                                       'profile[bio]': '',
+                                       'profile[location]': '',
+                                       'profile[gender]': '',
+                                       'profile[date][year]': '',
+                                       'profile[date][month]': '',
+                                       'profile[date][day]': '',
+                                       }
+               self._html = self._fetchhtml()
+               self._loaded = False
+               if not no_load: self.load()
+
+       def _fetchhtml(self):
+               """Fetches html that will be used to extract data.
+               """
+               return self._connection.get('profile/edit').text
+
+       def getName(self):
+               """Returns two-tuple: (first, last) name.
+               """
+               first = self.firstname_regexp.search(self._html).group(1)
+               last = self.lastname_regexp.search(self._html).group(1)
+               return (first, last)
+
+       def getTags(self):
+               """Returns tags user had selected when describing him/her-self.
+               """
+               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)]
+
+       def getBio(self):
+               """Returns user bio.
+               """
+               bio = self.bio_regexp.search(self._html).group(1)
+               return bio
+
+       def getLocation(self):
+               """Returns location string.
+               """
+               location = self.location_regexp.search(self._html).group(1)
+               return location
+
+       def getGender(self):
+               """Returns location string.
+               """
+               gender = self.gender_regexp.search(self._html).group(1)
+               return gender
+
+       def getBirthDate(self, named_month=False):
+               """Returns three-tuple: (year, month, day).
+
+               :param named_month: if True, return name of the month instead of integer
+               :type named_month: bool
+               """
+               year = self.birth_year_regexp.search(self._html)
+               if year is None: year = -1
+               else: year = int(year.group(1))
+               month = self.birth_month_regexp.search(self._html)
+               if month is None:
+                       if named_month: month = ''
+                       else: month = -1
+               else:
+                       if named_month:
+                               month = month.group(2)
+                       else:
+                               month = int(month.group(1))
+               day = self.birth_day_regexp.search(self._html)
+               if day is None: day = -1
+               else: day = int(day.group(1))
+               return (year, month, day)
+
+       def isSearchable(self):
+               """Returns True if profile is searchable.
+               """
+               searchable = self.is_searchable_regexp.search(self._html)
+               # this is because value="true" in every case so we just
+               # check if the field is "checked"
+               if searchable is None: searchable = False  # if it isn't - the regexp just won't match
+               else: searchable = True
+               return searchable
+
+       def isNSFW(self):
+               """Returns True if profile is marked as NSFW.
+               """
+               nsfw = self.is_nsfw_regexp.search(self._html)
+               if nsfw is None: nsfw = False
+               else: nsfw = True
+               return nsfw
+
+       def setName(self, first, last):
+               """Set first and last name.
+               """
+               self.data['profile[first_name]'] = first
+               self.data['profile[last_name]'] = last
+
+       def setTags(self, tags):
+               """Sets tags that describe the user.
+               """
+               self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
+
+       def setBio(self, bio):
+               """Set bio of a user.
+               """
+               self.data['profile[bio]'] = bio
+
+       def setLocation(self, location):
+               """Set location of a user.
+               """
+               self.data['profile[location]'] = location
+
+       def setGender(self, gender):
+               """Set gender of a user.
+               """
+               self.data['profile[gender]'] = gender
+
+       def setBirthDate(self, year, month, day):
+               """Set birth date of a user.
+               """
+               self.data['profile[date][year]'] = year
+               self.data['profile[date][month]'] = month
+               self.data['profile[date][day]'] = day
+
+       def setSearchable(self, searchable):
+               """Set user's searchable status.
+               """
+               self.data['profile[searchable]'] = json.dumps(searchable)
+
+       def setNSFW(self, nsfw):
+               """Set user NSFW status.
+               """
+               self.data['profile[nsfw]'] = json.dumps(nsfw)
+
+       def load(self):
+               """Loads profile data into self.data dictionary.
+               **Notice:** Not all keys are loaded yet.
+               """
+               self.setName(*self.getName())
+               self.setBio(self.getBio())
+               self.setLocation(self.getLocation())
+               self.setGender(self.getGender())
+               self.setBirthDate(*self.getBirthDate(named_month=False))
+               self.setSearchable(self.isSearchable())
+               self.setNSFW(self.isNSFW())
+               self.setTags(self.getTags())
+               self._loaded = True
+
+       def update(self):
+               """Updates profile information.
+               """
+               if not self._loaded: raise errors.DiaspyError('profile was not loaded')
+               self.data['authenticity_token'] = repr(self._connection)
+               print(self.data)
+               request = self._connection.post('profile', data=self.data, allow_redirects=False)
+               return request.status_code
 
 
 class Services():
-    """Provides interface to services settings.
-    """
-    def __init__(self, connection):
-        self._connection = connection
+       """Provides interface to services settings.
+       """
+       def __init__(self, connection):
+               self._connection = connection
index c9ca429a903827c26a12052a33a46af4508d5170..b00139c371ea49d3c4b7d7f9a617536ff01fb83f 100644 (file)
@@ -20,521 +20,521 @@ We need this to get a UTC timestamp from the latest loaded post in the
 stream, so we can use it for the more() function.
 """
 try:
-    import dateutil.parser
-    def parse_utc_timestamp(date_str):
-        return round(dateutil.parser.parse(date_str).timestamp())
+       import dateutil.parser
+       def parse_utc_timestamp(date_str):
+               return round(dateutil.parser.parse(date_str).timestamp())
 
 except ImportError:
-    try:
-        from datetime import datetime
-        from pytz import timezone
-        def parse_utc_timestamp(date_str):
-            return round(datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone('UTC')).timestamp())
+       try:
+               from datetime import datetime
+               from pytz import timezone
+               def parse_utc_timestamp(date_str):
+                       return round(datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone('UTC')).timestamp())
 
-    except ImportError:
-        print("Please install either python-dateutil or python-pytz")
-        exit # TODO raise exception
+       except ImportError:
+               print("Please install either python-dateutil or python-pytz")
+               exit # TODO raise exception
 
 class Generic():
-    """Object representing generic stream.
-    """
-    _location = 'stream.json'
-
-    def __init__(self, connection, location='', fetch=True):
-        """
-        :param connection: Connection() object
-        :type connection: diaspy.connection.Connection
-        :param location: location of json (optional)
-        :type location: str
-        :param fetch: will call .fill() if true
-        :type fetch: bool
-        """
-        self._connection = connection
-        if location: self._location = location
-        self.latest = None
-        self._stream = []
-        #   since epoch
-        self.max_time = int(time.mktime(time.gmtime()))
-        if fetch: self.fill()
-
-    def __contains__(self, post):
-        """Returns True if stream contains given post.
-        """
-        return post in self._stream
-
-    def __iter__(self):
-        """Provides iterable interface for stream.
-        """
-        return iter(self._stream)
-
-    def __getitem__(self, n):
-        """Returns n-th item in Stream.
-        """
-        return self._stream[n]
-
-    def __len__(self):
-        """Returns length of the Stream.
-        """
-        return len(self._stream)
-
-    def _obtain(self, max_time=0, suppress=True):
-        """Obtains stream from pod.
-
-        suppress:bool - suppress post-fetching errors (e.g. 404)
-        """
-        params = {}
-        if max_time:
-            if self.latest == None:
-                self.latest = int(time.mktime(time.gmtime()) * 1000)
-                self.latest -= max_time
-            else: self.latest += 1
-            params['max_time'] = max_time
-            params['_'] = self.latest
-            print("Diaspy _obtain.params: {}".format(params))
-        request = self._connection.get(self._location, params=params)
-        if request.status_code != 200:
-            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))
-                if post['created_at']: latest_time = post['created_at']
-            except errors.PostError:
-                if not suppress:
-                    raise
-        if latest_time:
-            self.max_time = parse_utc_timestamp( latest_time )
-        return posts
-
-    def _expand(self, new_stream):
-        """Appends older posts to stream.
-        """
-        guids = [post.guid for post in self._stream]
-        stream = self._stream
-        for post in new_stream:
-            if post.guid not in guids:
-                stream.append(post)
-                guids.append(post.guid)
-        self._stream = stream
-
-    def _update(self, new_stream):
-        """Updates stream with new posts.
-        """
-        guids = [post.guid for post in self._stream]
-
-        stream = self._stream
-        for i in range(len(new_stream)):
-            if new_stream[-i].guid not in guids:
-                stream = [new_stream[-i]] + stream
-                guids.append(new_stream[-i].guid)
-        self._stream = stream
-
-    def clear(self):
-        """Set stream to empty.
-        """
-        self._stream = []
-
-    def purge(self):
-        """Removes all unexistent posts from stream.
-        """
-        stream = []
-        for post in self._stream:
-            deleted = False
-            try:
-                # error will tell us that the post has been deleted
-                post.update()
-            except Exception:
-                deleted = True
-            finally:
-                if not deleted: stream.append(post)
-        self._stream = stream
-
-    def update(self):
-        """Updates stream with new posts.
-        """
-        self._update(self._obtain())
-
-    def fill(self):
-        """Fills the stream with posts.
-
-        **Notice:** this will create entirely new list of posts.
-        If you want to preseve posts already present in stream use update().
-        """
-        self._stream = self._obtain()
-
-    def more(self, max_time=0, backtime=86400):
-        """Tries to download more (older posts) posts from Stream.
-
-        TODO backtime isn't used anymore.
-        Diaspora reference: https://github.com/diaspora/diaspora/blob/26a9e50ef935628c800f9a21d345057556fa5c31/app/helpers/stream_helper.rb#L48
-
-        :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
-        """
-
-        if not max_time: max_time = self.max_time
-        self.max_time = max_time
-        new_stream = self._obtain(max_time=max_time)
-        self._expand(new_stream)
-
-    def full(self, backtime=86400, 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 (in the beginning of my D* activity I was posting 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.
-
-        Default retry is 42. If you don't know why go to the nearest library (or to the nearest
-        Piratebay mirror) and grab a copy of "A Hitchhiker's Guide to the Galaxy" and read the
-        book to find out. This will also increase your level of geekiness and you'll have a
-        great time reading the book.
-
-        :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 backtime, keep calm and
-                # try going further back in time...
-                n -= 1
-            # check the comment below
-            # no commented code should be present in good software
-            #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, **kwargs):
-        """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, **kwargs)
+       """Object representing generic stream.
+       """
+       _location = 'stream.json'
+
+       def __init__(self, connection, location='', fetch=True):
+               """
+               :param connection: Connection() object
+               :type connection: diaspy.connection.Connection
+               :param location: location of json (optional)
+               :type location: str
+               :param fetch: will call .fill() if true
+               :type fetch: bool
+               """
+               self._connection = connection
+               if location: self._location = location
+               self.latest = None
+               self._stream = []
+               #   since epoch
+               self.max_time = int(time.mktime(time.gmtime()))
+               if fetch: self.fill()
+
+       def __contains__(self, post):
+               """Returns True if stream contains given post.
+               """
+               return post in self._stream
+
+       def __iter__(self):
+               """Provides iterable interface for stream.
+               """
+               return iter(self._stream)
+
+       def __getitem__(self, n):
+               """Returns n-th item in Stream.
+               """
+               return self._stream[n]
+
+       def __len__(self):
+               """Returns length of the Stream.
+               """
+               return len(self._stream)
+
+       def _obtain(self, max_time=0, suppress=True):
+               """Obtains stream from pod.
+
+               suppress:bool - suppress post-fetching errors (e.g. 404)
+               """
+               params = {}
+               if max_time:
+                       if self.latest == None:
+                               self.latest = int(time.mktime(time.gmtime()) * 1000)
+                               self.latest -= max_time
+                       else: self.latest += 1
+                       params['max_time'] = max_time
+                       params['_'] = self.latest
+                       print("Diaspy _obtain.params: {}".format(params))
+               request = self._connection.get(self._location, params=params)
+               if request.status_code != 200:
+                       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))
+                               if post['created_at']: latest_time = post['created_at']
+                       except errors.PostError:
+                               if not suppress:
+                                       raise
+               if latest_time:
+                       self.max_time = parse_utc_timestamp( latest_time )
+               return posts
+
+       def _expand(self, new_stream):
+               """Appends older posts to stream.
+               """
+               guids = [post.guid for post in self._stream]
+               stream = self._stream
+               for post in new_stream:
+                       if post.guid not in guids:
+                               stream.append(post)
+                               guids.append(post.guid)
+               self._stream = stream
+
+       def _update(self, new_stream):
+               """Updates stream with new posts.
+               """
+               guids = [post.guid for post in self._stream]
+
+               stream = self._stream
+               for i in range(len(new_stream)):
+                       if new_stream[-i].guid not in guids:
+                               stream = [new_stream[-i]] + stream
+                               guids.append(new_stream[-i].guid)
+               self._stream = stream
+
+       def clear(self):
+               """Set stream to empty.
+               """
+               self._stream = []
+
+       def purge(self):
+               """Removes all unexistent posts from stream.
+               """
+               stream = []
+               for post in self._stream:
+                       deleted = False
+                       try:
+                               # error will tell us that the post has been deleted
+                               post.update()
+                       except Exception:
+                               deleted = True
+                       finally:
+                               if not deleted: stream.append(post)
+               self._stream = stream
+
+       def update(self):
+               """Updates stream with new posts.
+               """
+               self._update(self._obtain())
+
+       def fill(self):
+               """Fills the stream with posts.
+
+               **Notice:** this will create entirely new list of posts.
+               If you want to preseve posts already present in stream use update().
+               """
+               self._stream = self._obtain()
+
+       def more(self, max_time=0, backtime=86400):
+               """Tries to download more (older posts) posts from Stream.
+
+               TODO backtime isn't used anymore.
+               Diaspora reference: https://github.com/diaspora/diaspora/blob/26a9e50ef935628c800f9a21d345057556fa5c31/app/helpers/stream_helper.rb#L48
+
+               :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
+               """
+
+               if not max_time: max_time = self.max_time
+               self.max_time = max_time
+               new_stream = self._obtain(max_time=max_time)
+               self._expand(new_stream)
+
+       def full(self, backtime=86400, 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 (in the beginning of my D* activity I was posting 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.
+
+               Default retry is 42. If you don't know why go to the nearest library (or to the nearest
+               Piratebay mirror) and grab a copy of "A Hitchhiker's Guide to the Galaxy" and read the
+               book to find out. This will also increase your level of geekiness and you'll have a
+               great time reading the book.
+
+               :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 backtime, keep calm and
+                               # try going further back in time...
+                               n -= 1
+                       # check the comment below
+                       # no commented code should be present in good software
+                       #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, **kwargs):
+               """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, **kwargs)
 
 
 class Outer(Generic):
-    """Object used by diaspy.models.User to represent
-    stream of other user.
-    """
-    def __init__(self, connection, guid, fetch=True):
-        location = 'people/{}/stream.json'.format(guid)
-        super().__init__(connection, location, fetch)
+       """Object used by diaspy.models.User to represent
+       stream of other user.
+       """
+       def __init__(self, connection, guid, fetch=True):
+               location = 'people/{}/stream.json'.format(guid)
+               super().__init__(connection, location, fetch)
 
 class Stream(Generic):
-    """The main stream containing the combined posts of the
-    followed users and tags and the community spotlights posts
-    if the user enabled those.
-    """
-    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=''):
-        """This function sends a post to an aspect.
-        If both `photo` and `photos` are specified `photos` takes precedence.
-
-        :param text: Text to post.
-        :type text: str
-
-        :param aspect_ids: Aspect ids to send post to.
-        :type aspect_ids: str
-
-        :param photo: filename of photo to post
-        :type photo: str
-
-        :param photos: id of photo to post (obtained from _photoupload())
-        :type photos: int
-
-        :param provider_display_name: name of provider displayed under the post
-        :type provider_display_name: str
-
-        :param poll_question: Question string
-        :type poll_question: str
-
-        :param poll_answers: Anwsers to the poll
-        :type poll_answers: list with strings
-
-        :param location_coords: TODO
-        :type location_coords: TODO
-
-        :returns: diaspy.models.Post -- the Post which has been created
-        """
-        data = {}
-        data['aspect_ids'] = aspect_ids
-        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:
-            data['poll_question'] = poll_question
-            data['poll_answers'] = 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)})
-        if request.status_code != 201:
-            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)
-        return post
-
-    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
-        """
-        data = open(filename, 'rb')
-        image = data.read()
-        data.close()
-
-        params = {}
-        params['photo[pending]'] = 'true'
-        params['set_profile_image'] = ''
-        params['qqfile'] = filename
-        if not aspects: aspects = self._connection.getUserData()['aspects']
-        for i, aspect in enumerate(aspects):
-            params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
-
-        headers = {'content-type': 'application/octet-stream',
-                   '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 errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
-        return request.json()['data']['photo']['id']
+       """The main stream containing the combined posts of the
+       followed users and tags and the community spotlights posts
+       if the user enabled those.
+       """
+       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=''):
+               """This function sends a post to an aspect.
+               If both `photo` and `photos` are specified `photos` takes precedence.
+
+               :param text: Text to post.
+               :type text: str
+
+               :param aspect_ids: Aspect ids to send post to.
+               :type aspect_ids: str
+
+               :param photo: filename of photo to post
+               :type photo: str
+
+               :param photos: id of photo to post (obtained from _photoupload())
+               :type photos: int
+
+               :param provider_display_name: name of provider displayed under the post
+               :type provider_display_name: str
+
+               :param poll_question: Question string
+               :type poll_question: str
+
+               :param poll_answers: Anwsers to the poll
+               :type poll_answers: list with strings
+
+               :param location_coords: TODO
+               :type location_coords: TODO
+
+               :returns: diaspy.models.Post -- the Post which has been created
+               """
+               data = {}
+               data['aspect_ids'] = aspect_ids
+               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:
+                       data['poll_question'] = poll_question
+                       data['poll_answers'] = 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)})
+               if request.status_code != 201:
+                       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)
+               return post
+
+       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
+               """
+               data = open(filename, 'rb')
+               image = data.read()
+               data.close()
+
+               params = {}
+               params['photo[pending]'] = 'true'
+               params['set_profile_image'] = ''
+               params['qqfile'] = filename
+               if not aspects: aspects = self._connection.getUserData()['aspects']
+               for i, aspect in enumerate(aspects):
+                       params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
+
+               headers = {'content-type': 'application/octet-stream',
+                                  '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 errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
+               return request.json()['data']['photo']['id']
 
 
 class Activity(Stream):
-    """Stream representing user's activity.
-    """
-    _location = 'activity.json'
-
-    def _delid(self, id):
-        """Deletes post with given id.
-        """
-        post = None
-        for p in self._stream:
-            if p['id'] == id:
-                post = p
-                break
-        if post is not None: post.delete()
-
-    def delete(self, post):
-        """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 purged.
-
-        :param post: post identifier
-        :type post: str, diaspy.models.Post
-        """
-        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.purge()
+       """Stream representing user's activity.
+       """
+       _location = 'activity.json'
+
+       def _delid(self, id):
+               """Deletes post with given id.
+               """
+               post = None
+               for p in self._stream:
+                       if p['id'] == id:
+                               post = p
+                               break
+               if post is not None: post.delete()
+
+       def delete(self, post):
+               """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 purged.
+
+               :param post: post identifier
+               :type post: str, diaspy.models.Post
+               """
+               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.purge()
 
 
 class Aspects(Generic):
-    """This stream contains the posts filtered by
-    the specified aspect IDs. You can choose the aspect IDs with
-    the parameter `aspect_ids` which value should be
-    a comma seperated list of aspect IDs.
-    If the parameter is ommitted all aspects are assumed.
-    An example call would be `aspects.json?aspect_ids=23,5,42`
-    """
-    _location = 'aspects.json'
-
-    def getAspectID(self, aspect_name):
-        """Returns id of an aspect of given name.
-        Returns -1 if aspect is not found.
-
-        :param aspect_name: aspect name (must be spelled exactly as when created)
-        :type aspect_name: str
-        :returns: int
-        """
-        id = -1
-        aspects = self._connection.getUserData()['aspects']
-        for aspect in aspects:
-            if aspect['name'] == aspect_name: id = aspect['id']
-        return id
-
-    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?a_ids[]=' + '{}'.format('&a_ids[]='.join(ids))
-        self.fill() # this will create entirely new list of posts.
-
-    def add(self, aspect_name, visible=0):
-        """This function adds a new aspect.
-        Status code 422 is accepted because it is returned by D* when
-        you try to add aspect already present on your aspect list.
-
-        :param aspect_name: name of aspect to create
-        :param visible: whether the contacts in this aspect are visible to each other or not
-
-        :returns: Aspect() object of just created aspect
-        """
-        data = {'authenticity_token': repr(self._connection),
-                'aspect[name]': aspect_name,
-                'aspect[contacts_visible]': visible}
-
-        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))
-
-        id = self.getAspectID(aspect_name)
-        return Aspect(self._connection, id)
-
-    def remove(self, id=-1, name=''):
-        """This method removes an aspect.
-        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
-        :type aspect_id: int
-        :param name: name of aspect to remove
-        :type name: str
-        """
-        if id == -1 and name: id = self.getAspectID(name)
-        data = {'_method': 'delete',
-                '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))
+       """This stream contains the posts filtered by
+       the specified aspect IDs. You can choose the aspect IDs with
+       the parameter `aspect_ids` which value should be
+       a comma seperated list of aspect IDs.
+       If the parameter is ommitted all aspects are assumed.
+       An example call would be `aspects.json?aspect_ids=23,5,42`
+       """
+       _location = 'aspects.json'
+
+       def getAspectID(self, aspect_name):
+               """Returns id of an aspect of given name.
+               Returns -1 if aspect is not found.
+
+               :param aspect_name: aspect name (must be spelled exactly as when created)
+               :type aspect_name: str
+               :returns: int
+               """
+               id = -1
+               aspects = self._connection.getUserData()['aspects']
+               for aspect in aspects:
+                       if aspect['name'] == aspect_name: id = aspect['id']
+               return id
+
+       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?a_ids[]=' + '{}'.format('&a_ids[]='.join(ids))
+               self.fill() # this will create entirely new list of posts.
+
+       def add(self, aspect_name, visible=0):
+               """This function adds a new aspect.
+               Status code 422 is accepted because it is returned by D* when
+               you try to add aspect already present on your aspect list.
+
+               :param aspect_name: name of aspect to create
+               :param visible: whether the contacts in this aspect are visible to each other or not
+
+               :returns: Aspect() object of just created aspect
+               """
+               data = {'authenticity_token': repr(self._connection),
+                               'aspect[name]': aspect_name,
+                               'aspect[contacts_visible]': visible}
+
+               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))
+
+               id = self.getAspectID(aspect_name)
+               return Aspect(self._connection, id)
+
+       def remove(self, id=-1, name=''):
+               """This method removes an aspect.
+               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
+               :type aspect_id: int
+               :param name: name of aspect to remove
+               :type name: str
+               """
+               if id == -1 and name: id = self.getAspectID(name)
+               data = {'_method': 'delete',
+                               '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))
 
 
 class Commented(Generic):
-    """This stream contains all posts
-    the user has made a comment on.
-    """
-    _location = 'commented.json'
+       """This stream contains all posts
+       the user has made a comment on.
+       """
+       _location = 'commented.json'
 
 
 class Liked(Generic):
-    """This stream contains all posts the user liked.
-    """
-    _location = 'liked.json'
+       """This stream contains all posts the user liked.
+       """
+       _location = 'liked.json'
 
 
 class Mentions(Generic):
-    """This stream contains all posts
-    the user is mentioned in.
-    """
-    _location = 'mentions.json'
+       """This stream contains all posts
+       the user is mentioned in.
+       """
+       _location = 'mentions.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
-        """
-        data = {'authenticity_token': self._connection.get_token()}
-        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))
-
-    def add(self, tag_name):
-        """Follow new tag.
-        Error code 403 is accepted because pods respod with it when request
-        is sent to follow a tag that a user already follows.
-
-        :param tag_name: tag name
-        :type tag_name: str
-        :returns: int (response code)
-        """
-        data = {'name': tag_name,
-                'authenticity_token': repr(self._connection),
-                }
-        headers = {'content-type': 'application/json',
-                   'x-csrf-token': repr(self._connection),
-                   'accept': 'application/json'
-                   }
-
-        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))
-        return request.status_code
+       """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
+               """
+               data = {'authenticity_token': self._connection.get_token()}
+               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))
+
+       def add(self, tag_name):
+               """Follow new tag.
+               Error code 403 is accepted because pods respod with it when request
+               is sent to follow a tag that a user already follows.
+
+               :param tag_name: tag name
+               :type tag_name: str
+               :returns: int (response code)
+               """
+               data = {'name': tag_name,
+                               'authenticity_token': repr(self._connection),
+                               }
+               headers = {'content-type': 'application/json',
+                                  'x-csrf-token': repr(self._connection),
+                                  'accept': 'application/json'
+                                  }
+
+               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))
+               return request.status_code
 
 
 class Tag(Generic):
-    """This stream contains all posts containing a tag.
-    """
-    def __init__(self, connection, tag, fetch=True):
-        """
-        :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)
-        if fetch: self.fill()
+       """This stream contains all posts containing a tag.
+       """
+       def __init__(self, connection, tag, fetch=True):
+               """
+               :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)
+               if fetch: self.fill()