text: from __future__ import unicode_literals
  
  import logging
  from xml.dom.minidom import parseString
  
  from django import forms
  from django.utils import six
  from django.utils.six.moves.urllib.error import HTTPError, URLError
  from django.utils.translation import ugettext_lazy as _, ugettext
  
  from reviewboard.hostingsvcs.errors import (AuthorizationError,
                                              HostingServiceAPIError,
                                              RepositoryError)
  from reviewboard.hostingsvcs.forms import (HostingServiceAuthForm,
                                             HostingServiceForm)
  from reviewboard.hostingsvcs.service import (HostingService,
                                               HostingServiceClient)
  from reviewboard.scmtools.crypto_utils import (decrypt_password,
                                                 encrypt_password)
  from reviewboard.scmtools.errors import FileNotFoundError
  
  
  class CodebaseHQAuthForm(HostingServiceAuthForm):
      api_key = forms.CharField(
          label=_('API key'),
          max_length=128,
          required=True,
          widget=forms.TextInput(attrs={'size': '60'}),
          help_text=_('The API key provided to your Codebase account. This is '
                      'available in My Profile under API Credentials.'))
  
      domain = forms.CharField(
          label=_('Codebase domain'),
          max_length=128,
          required=True,
          widget=forms.TextInput(attrs={'size': '60'}),
          help_text=_('The subdomain used to access your Codebase account. '
                      'This is the "<tt>subdomain</tt>" of '
                      '<tt>subdomain</tt>.codebasehq.com.'))
  
      def get_credentials(self):
          credentials = super(CodebaseHQAuthForm, self).get_credentials()
          credentials.update({
              'domain': self.cleaned_data['domain'],
              'api_key': self.cleaned_data['api_key'],
          })
  
          return credentials
  
      class Meta(object):
          help_texts = {
              'hosting_account_username': _(
                  'The username you use to log into Codebase. This should '
                  '<em>not</em> include the domain name.'
              ),
              'hosting_account_password': _(
                  'The password you use to log into Codebase. This is separate '
                  'from the API key below.'
              ),
          }
  
  
  class CodebaseHQForm(HostingServiceForm):
      codebasehq_project_name = forms.CharField(
          label=_('Project name'),
          max_length=64,
          required=True,
          widget=forms.TextInput(attrs={'size': '60'}))
  
      codebasehq_repo_name = forms.CharField(
          label=_('Repository short name'),
          max_length=128,
          required=True,
          widget=forms.TextInput(attrs={'size': '60'}),
          help_text=_('The short name of your repository. This can be found by '
                      'clicking the Settings button on the right-hand '
                      'side of the repository browser.'))
  
  
  class CodebaseHQClient(HostingServiceClient):
      """Client for talking to the Codebase API.
  
      This implements the API methods that the hosting service needs, converting
      requests into API calls and those back into structured results.
      """
  
      #: Mimetype used for API requests and responses.
      API_MIMETYPE = 'application/xml'
  
      def __init__(self, hosting_service):
          """Initialize the client.
  
          Args:
              hosting_service (CodebaseHQ):
                  The hosting service that owns this client.
          """
          self.hosting_service = hosting_service
  
      def api_get_file(self, repository, project_name, repo_name, path,
                       revision):
          """Return the content of a file in a repository.
  
          Args:
              repository (reviewboard.scmtools.models.Repository):
                  The repository entry in Review Board.
  
              project_name (unicode):
                  The name of the Codebase project.
  
              repo_name (unicode):
                  The name of the repository.
  
              path (unicode):
                  The path to the file in the repository.
  
              revision (unicode):
                  The revision of the file or commit.
  
          Returns:
              bytes:
              The contents of the file.
          """
          url = '%s/%s/blob/' % (project_name, repo_name)
  
          if repository.tool.name == 'Git':
              url += revision
          else:
              if path.startswith('/'):
                  path = path[1:]
  
              url += '%s/%s' % (revision, path)
  
          return self.api_get(self.build_api_url(url), raw_content=True)
  
      def api_get_public_keys(self, username):
          """Return information on all public keys for a user.
  
          Args:
              username (unicode):
                  The user to fetch public keys for.
  
          Returns:
              dict:
              Information on each of the user's public keys.
          """
          return self.api_get(self.build_api_url('users/%s/public_keys'
                                                 % username))
  
      def api_get_repository(self, project_name, repo_name):
          """Return information on a repository.
  
          Args:
              project_name (unicode):
                  The name of the Codebase project.
  
              repo_name (unicode):
                  The name of the repository.
  
          Returns:
              dict:
              Information on the repository.
  
              See https://support.codebasehq.com/kb/repositories for the
              data returned.
          """
          return self.api_get(
              self.build_api_url('%s/%s' % (project_name, repo_name)))
  
      def build_api_url(self, url):
          """Return the URL for an API call.
  
          Args:
              url (unicode):
                  The relative URL for the API call.
  
          Returns:
              unicode:
              The absolute URL for the API call.
          """
          return 'https://api3.codebasehq.com/%s' % url
  
      def api_get(self, url, raw_content=False):
          """Perform an HTTP GET request to the API.
  
          Args:
              url (unicode):
                  The full URL to the API resource.
  
              raw_content (bool, optional):
                  If set to ``True``, the raw content of the result will be
                  returned, instead of a parsed XML result.
  
          Returns:
              object:
              The parsed content of the result, as a dictionary, or the raw
              bytes content if ``raw_content`` is ``True``.
          """
          hosting_service = self.hosting_service
  
          try:
              account_data = hosting_service.account.data
              api_username = '%s/%s' % (account_data['domain'],
                                        hosting_service.account.username)
              api_key = decrypt_password(account_data['api_key'])
  
              response = self.http_get(
                  url,
                  username=api_username,
                  password=api_key,
                  headers={
                      'Accept': self.API_MIMETYPE,
                  })
              data = response.data
  
              if raw_content:
                  return data
              else:
                  return self.parse_xml(data)
          except HTTPError as e:
              data = e.read()
              msg = six.text_type(e)
  
              rsp = self.parse_xml(data)
  
              if rsp and 'errors' in rsp:
                  errors = rsp['errors']
  
                  if 'error' in errors:
                      msg = errors['error']
  
              if e.code == 401:
                  raise AuthorizationError(msg)
              else:
                  raise HostingServiceAPIError(msg, http_code=e.code, rsp=rsp)
          except URLError as e:
              raise HostingServiceAPIError(e.reason)
  
      def get_xml_text(self, nodes):
          """Return the text contents of a set of XML nodes.
  
          Args:
              nodes (list of xml.dom.minidom.Element):
                  The list of nodes.
  
          Returns:
              unicode:
              The text content of the nodes.
          """
          return ''.join(
              node.data
              for node in nodes
              if node.nodeType == node.TEXT_NODE
          )
  
      def parse_xml(self, s):
          """Return the parsed content for an XML document.
  
          Args:
              s (unicode):
                  The XML document as a string.
  
          Returns:
              dict:
              The parsed content of the XML document, with each key
              being a dictionary of other parsed content.
  
              If the document cannot be parsed, this will return ``None``.
          """
          try:
              doc = parseString(s)
          except:
              return None
  
          root = doc.documentElement
  
          return {
              root.tagName: self._parse_xml_node(root),
          }
  
      def _parse_xml_node(self, node):
          """Return the parsed content for a node in an XML document.
  
          This parses the content of a Codebase XML document, turning it into
          arrays, strings, and dictionaries of data.
  
          Args:
              node (xml.dom.minidom.Element):
                  The node being parsed.
  
          Returns:
              object:
              The parsed content of the node, based on the type of node being
              processed.
          """
          node_type = node.getAttribute('type')
          is_nil = node.getAttribute('nil')
  
          if node_type == 'array':
              result = [
                  self._parse_xml_node(child)
                  for child in node.childNodes
                  if child.nodeType == child.ELEMENT_NODE
              ]
          elif is_nil == 'true':
              result = None
          else:
              child_nodes = [
                  child
                  for child in node.childNodes
                  if child.nodeType == child.ELEMENT_NODE
              ]
  
              if child_nodes:
                  result = dict([
                      (child.tagName, self._parse_xml_node(child))
                      for child in child_nodes
                  ])
              else:
                  result = self.get_xml_text(node.childNodes)
  
          return result
  
  
  class CodebaseHQ(HostingService):
      """Repository hosting support for Codebase.
  
      Codebase is a repository hosting service that supports Subversion, Git,
      and Mercurial. It's available at https://codebasehq.com.
  
      This integration provides repository validation and file fetching. Due to
      API limitations, it does not support post-commit review at this time.
      """
  
      name = 'Codebase HQ'
      form = CodebaseHQForm
      auth_form = CodebaseHQAuthForm
  
      needs_authorization = True
      supports_bug_trackers = True
      supports_repositories = True
  
      supported_scmtools = ['Git', 'Subversion', 'Mercurial']
  
      repository_fields = {
          'Git': {
              'path': 'git@codebasehq.com:%(domain)s/'
                      '%(codebasehq_project_name)s/'
                      '%(codebasehq_repo_name)s.git',
          },
          'Subversion': {
              'path': 'https://%(domain)s.codebasehq.com/'
                      '%(codebasehq_project_name)s/'
                      '%(codebasehq_repo_name)s.svn',
          },
          'Mercurial': {
              'path': 'https://%(domain)s.codebasehq.com/'
                      'projects/%(codebasehq_project_name)s/repositories/'
                      '%(codebasehq_repo_name)s/',
          },
      }
  
      bug_tracker_field = (
          'https://%(domain)s.codebasehq.com/projects/'
          '%(codebasehq_project_name)s/tickets/%%s'
      )
  
      #: A mapping of Codebase SCM types to SCMTool names.
      REPO_SCM_TOOL_MAP = {
          'git': 'Git',
          'svn': 'Subversion',
          'hg': 'Mercurial',
      }
  
      def __init__(self, *args, **kwargs):
          """Initialize the hosting service.
  
          Args:
              *args (tuple):
                  Positional arguments for the parent constructor.
  
              **kwargs (dict):
                  Keyword arguments for the parent constructor.
          """
          super(CodebaseHQ, self).__init__(*args, **kwargs)
  
          self.client = CodebaseHQClient(self)
  
      def authorize(self, username, password, credentials, *args, **kwargs):
          """Authorize an account for Codebase.
  
          Codebase usees HTTP Basic Auth with an API username (consisting of the
          Codebase team's domain and the account username) and an API key (for
          the password) for API calls, and a standard username/password for
          Subversion repository access. We need to store all of this.
  
          Args:
              username (unicode):
                  The username to authorize.
  
              password (unicode):
                  The API token used as a password.
  
              credentials (dict):
                  Additional credentials from the authentication form.
  
              *args (tuple):
                  Extra unused positional arguments.
  
              **kwargs (dict):
                  Extra unused keyword arguments.
  
          Raises:
              reviewboard.hostingsvcs.errors.AuthorizationError:
                  The credentials provided were not valid.
          """
          self.account.data.update({
              'domain': credentials['domain'],
              'api_key': encrypt_password(credentials['api_key']),
              'password': encrypt_password(password),
          })
  
          # Test the account to make sure the credentials are fine. Note that
          # we can only really sanity-check the API token, domain, and username
          # from here. There's no way good way to check the actual password,
          # which we only use for Subversion repositories.
          #
          # This will raise a suitable error message if authorization fails.
          try:
              self.client.api_get_public_keys(username)
          except AuthorizationError:
              raise AuthorizationError(
                  ugettext('One or more of the credentials provided were not '
                           'accepted by Codebase.'))
  
          self.account.save()
  
      def is_authorized(self):
          """Return if the account has been authorized.
  
          This checks if all the modern authentication details are stored along
          with the account.
  
          Returns:
              bool:
              ``True`` if all required credentials are set for the account.
          """
          return (self.account.data.get('api_key') is not None and
                  self.account.data.get('password') is not None and
                  self.account.data.get('domain') is not None)
  
      def get_password(self):
          """Return the password for this account.
  
          This is used primarily for Subversion repositories, so that direct
          access can be performed in order to fetch properties and other
          information.
  
          This does not return the API key.
  
          Returns:
              unicode:
              The account password for repository access.
          """
          return decrypt_password(self.account.data['password'])
  
      def check_repository(self, codebasehq_project_name=None,
                           codebasehq_repo_name=None, tool_name=None,
                           *args, **kwargs):
          """Check the validity of a repository.
  
          This will perform an API request against Codebase to get information on
          the repository. This will throw an exception if the repository was not
          found, and return cleanly if it was found.
  
          Args:
              codebase_project_name (unicode):
                  The name of the project on Codebase.
  
              codebasehq_repo_name (unicode):
                  The name of the repository on Codebase.
  
              tool_name (unicode):
                  The name of the SCMTool for the repository.
  
              *args (tuple):
                  Extra unused positional arguments passed to this function.
  
              **kwargs (dict):
                  Extra unused keyword arguments passed to this function.
  
          Raises:
              reviewboard.hostingsvcs.errors.RepositoryError:
                  The repository was not found.
          """
          # The form should enforce these values.
          assert codebasehq_project_name
          assert codebasehq_repo_name
          assert tool_name
  
          try:
              info = self.client.api_get_repository(codebasehq_project_name,
                                                    codebasehq_repo_name)
          except HostingServiceAPIError as e:
              logging.error('Error finding Codebase repository "%s" for '
                            'project "%s": %s',
                            codebasehq_repo_name, codebasehq_project_name,
                            e)
  
              raise RepositoryError(
                  ugettext('A repository with this name and project was '
                           'not found.'))
  
          try:
              scm_type = info['repository']['scm']
          except KeyError:
              logging.error('Missing "scm" field for Codebase HQ repository '
                            'payload: %r',
                            info)
  
              raise RepositoryError(
                  ugettext('Unable to determine the type of repository '
                           'from the Codebase API. Please report this.'))
  
          try:
              expected_tool_name = self.REPO_SCM_TOOL_MAP[scm_type]
          except KeyError:
              logging.error('Unexpected "scm" value "%s" for Codebase HQ '
                            'repository, using payload: %r',
                            scm_type, info)
  
              raise RepositoryError(
                  ugettext('Unable to determine the type of repository '
                           'from the Codebase API. Please report this.'))
  
          if expected_tool_name != tool_name:
              raise RepositoryError(
                  ugettext("The repository type doesn't match what you "
                           "selected. Did you mean %s?")
                  % expected_tool_name)
  
      def get_file(self, repository, path, revision, *args, **kwargs):
          """Returns the content of a file in a repository.
  
          This will perform an API request to fetch the contents of a file.
  
          Args:
              repository (reviewboard.scmtools.models.Repository):
                  The repository containing the file.
  
              path (unicode):
                  The path to the file in the repository.
  
              revision (unicode):
                  The revision of the file in the repository.
  
              *args (tuple):
                  Extra unused positional arguments passed to this function.
  
              **kwargs (dict):
                  Extra unused keyword arguments passed to this function.
  
          Returns:
              byets:
              The content of the file in the repository.
          """
          try:
              return self.client.api_get_file(
                  repository,
                  repository.extra_data['codebasehq_project_name'],
                  repository.extra_data['codebasehq_repo_name'],
                  path, revision)
          except HostingServiceAPIError as e:
              if e.http_code == 404:
                  raise FileNotFoundError(path, revision)
              else:
                  logging.warning('Failed to fetch file from Codebase HQ '
                                  'repository %s: %s',
                                  repository, e)
                  raise
  
      def get_file_exists(self, repository, path, revision, *args, **kwargs):
          """Returns whether a given file exists.
  
          This will perform an API request to fetch the contents of a file,
          returning ``True`` if the content could be fetched.
  
          Args:
              repository (reviewboard.scmtools.models.Repository):
                  The repository containing the file.
  
              path (unicode):
                  The path to the file in the repository.
  
              revision (unicode):
                  The revision of the file in the repository.
  
              *args (tuple):
                  Extra unused positional arguments passed to this function.
  
              **kwargs (dict):
                  Extra unused keyword arguments passed to this function.
  
          Returns:
              bool:
              ``True`` if the file exists in the repository.
          """
          try:
              self.client.api_get_file(
                  repository,
                  repository.extra_data['codebasehq_project_name'],
                  repository.extra_data['codebasehq_repo_name'],
                  path, revision)
  
              return True
          except HostingServiceAPIError:
              return False
  