docs: def _extractAssociation(self, assoc_response, assoc_session):
          """Attempt to extract an association from the response, given
          the association response message and the established
          association session.
  
          @param assoc_response: The association response message from
              the server
          @type assoc_response: openid.message.Message
  
          @param assoc_session: The association session object that was
              used when making the request
          @type assoc_session: depends on the session type of the request
  
          @raises ProtocolError: when data is malformed
          @raises KeyError: when a field is missing
  
          @rtype: openid.association.Association
          """
          # Extract the common fields from the response, raising an
          # exception if they are not found
          assoc_type = assoc_response.getArg(
              OPENID_NS, 'assoc_type', no_default)
          assoc_handle = assoc_response.getArg(
              OPENID_NS, 'assoc_handle', no_default)
  
          # expires_in is a base-10 string. The Python parsing will
          # accept literals that have whitespace around them and will
          # accept negative values. Neither of these are really in-spec,
          # but we think it's OK to accept them.
          expires_in_str = assoc_response.getArg(
              OPENID_NS, 'expires_in', no_default)
          try:
              expires_in = int(expires_in_str)
          except ValueError, why:
              raise ProtocolError('Invalid expires_in field: %s' % (why[0],))
  
          # OpenID 1 has funny association session behaviour.
          if assoc_response.isOpenID1():
              session_type = self._getOpenID1SessionType(assoc_response)
          else:
              session_type = assoc_response.getArg(
                  OPENID2_NS, 'session_type', no_default)
  
          # Session type mismatch
          if assoc_session.session_type != session_type:
              if (assoc_response.isOpenID1() and
                  session_type == 'no-encryption'):
                  # In OpenID 1, any association request can result in a
                  # 'no-encryption' association response. Setting
                  # assoc_session to a new no-encryption session should
                  # make the rest of this function work properly for
                  # that case.
                  assoc_session = PlainTextConsumerSession()
              else:
                  # Any other mismatch, regardless of protocol version
                  # results in the failure of the association session
                  # altogether.
                  fmt = 'Session type mismatch. Expected %r, got %r'
                  message = fmt % (assoc_session.session_type, session_type)
                  raise ProtocolError(message)
  
          # Make sure assoc_type is valid for session_type
          if assoc_type not in assoc_session.allowed_assoc_types:
              fmt = 'Unsupported assoc_type for session %s returned: %s'
              raise ProtocolError(fmt % (assoc_session.session_type, assoc_type))
  
          # Delegate to the association session to extract the secret
          # from the response, however is appropriate for that session
          # type.
          try:
              secret = assoc_session.extractSecret(assoc_response)
          except ValueError, why:
              fmt = 'Malformed response for %s session: %s'
              raise ProtocolError(fmt % (assoc_session.session_type, why[0]))
  
          return Association.fromExpiresIn(
              expires_in, assoc_handle, secret, assoc_type)