* Removed `backtime` parameter from `diaspy.streams.Generic()` it's `more()` method...
[diaspy.git] / diaspy / settings.py
index c926efdfa01bd21781ee04769f61b8243e934887..53d7ab5751198d54f651908226a3ecd5352f75ce 100644 (file)
 
 import json
 import os
-import re
 import urllib
 import warnings
 
+BS4_SUPPORT=False
+try:
+       from bs4 import BeautifulSoup
+except ImportError:
+       import re
+       print("[diaspy] BeautifulSoup not found, falling back on regex.")
+else: BS4_SUPPORT=True
+
 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.
+       """
+       if not BS4_SUPPORT:
+               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')
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(data.text, 'lxml')
+                       email = soup.find('input', {"id": "user_email"})
+                       if email: email = email['value']
+                       else: email = ''
+                       return email
+               else:
+                       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')
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(request.text, 'lxml')
+                       language = soup.find('select', {"id": "user_language"})
+                       return [(option.text, option['value']) for option in language.findAll('option')]
+               else:
+                       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.
+       """
+       if not BS4_SUPPORT:
+               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.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       first = soup.find('input', {"id": "profile_first_name"})
+                       last = soup.find('input', {"id": "profile_last_name"})
+                       return (first['value'], last['value'])
+               else:
+                       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
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(html, 'lxml')
+                       tags = soup.find('meta', {"name": "keywords"})
+                       return [tag.lower() for tag in tags['content'].split(", ")]
+               else:
+                       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.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       bio = soup.find('textarea', {"id": "profile_bio"})
+                       return bio.get_text()
+               else:
+                       bio = self.bio_regexp.search(self._html).group(1)
+                       return bio
+
+       def getLocation(self):
+               """Returns location string.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       location = soup.find('input', {"id": "profile_location"})
+                       return location['value']
+               else:
+                       location = self.location_regexp.search(self._html).group(1)
+                       return location
+
+       def getGender(self):
+               """Returns location string.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       gender = soup.find('input', {"id": "profile_gender"})
+                       return gender['value']
+               else:
+                       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
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+
+                       year = soup.find('select', {"id": "profile_date_year"})
+                       year_option = year.find('option', selected=True)
+                       if year_option is None: year_option = -1
+                       else: year_option = int(year_option['value'])
+
+                       month = soup.find('select', {"id": "profile_date_month"})
+                       month_option = month.find('option', selected=True)
+                       if month_option is None:
+                               if named_month: month_option = ''
+                               else: month_option = -1
+                       elif named_month:
+                               month_option = month_option.text
+                       else: month_option = month_option['value']
+
+                       day = soup.find('select', {"id": "profile_date_day"})
+                       day_option = day.find('option', selected=True)
+                       if day_option is None: day_option = -1
+                       else: day_option = int(day_option['value'])
+                       return (year_option, month_option, day_option)
+               else:
+                       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.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       searchable = soup.find('input', {"id": "profile_searchable"})
+                       if (searchable.has_attr('checked') and
+                                       searchable['checked'] == 'checked'):
+                               return True
+                       else: return False
+               else:
+                       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.
+               """
+               if BS4_SUPPORT:
+                       soup = BeautifulSoup(self._html, 'lxml')
+                       nsfw = soup.find('input', {"id": "profile_nsfw"})
+                       if (nsfw.has_attr('checked') and
+                                       nsfw['checked'] == 'checked'):
+                               return True
+                       else: return False
+               else:
+                       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)
+               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