diff --git a/requirements.txt b/requirements.txt
index 205e224ff82115d30a27c04876b3ac6861e66ec8..a79bcb224d8aa0d13239a7460e8eb1f1716c6f07 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 django>=1.11
 tox
+ibisclient~=1.3
diff --git a/setup.py b/setup.py
index 95d4565375a6ba469c6c952eaee6e15800682552..6518737724193348548b44df20f51be4f4a1580d 100755
--- a/setup.py
+++ b/setup.py
@@ -7,13 +7,13 @@ setup(
     long_description=open('README.md').read(),
     long_description_content_type='text/markdown',
     url='https://gitlab.developers.cam.ac.uk/uis/devops/django/ucamlookup',
-    version='3.0.4',
+    version='3.0.5',
     license='MIT',
     author='DevOps Division, University Information Services, University of Cambridge',
     author_email='devops@uis.cam.ac.uk',
     packages=find_packages(),
     include_package_data=True,
-    install_requires=['django>=1.11'],
+    install_requires=['django>=1.11', 'ibisclient~=1.3'],
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Web Environment',
diff --git a/ucamlookup/ibisclient.py b/ucamlookup/ibisclient.py
new file mode 100644
index 0000000000000000000000000000000000000000..d09ff4017469221033eaeaf2a4184cfe01e448ea
--- /dev/null
+++ b/ucamlookup/ibisclient.py
@@ -0,0 +1,3 @@
+# Compatibility module to provide ibisclient at the same location as when we
+# vendored it.
+from ibisclient import *  # noqa: F401, F403
diff --git a/ucamlookup/ibisclient/__init__.py b/ucamlookup/ibisclient/__init__.py
deleted file mode 100644
index c61ea9f674e85d762c7f5b3d435b5a8195d3ceb9..0000000000000000000000000000000000000000
--- a/ucamlookup/ibisclient/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# --------------------------------------------------------------------------
-# Copyright (c) 2012, University of Cambridge Computing Service
-#
-# This file is part of the Lookup/Ibis client library.
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
-# License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library.  If not, see <http://www.gnu.org/licenses/>.
-#
-# Author: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-# --------------------------------------------------------------------------
-
-from .connection import *
-from .dto import *
-from .methods import *
diff --git a/ucamlookup/ibisclient/cacerts.txt b/ucamlookup/ibisclient/cacerts.txt
deleted file mode 100644
index 104f643e4760c05abda3a2ebdc89c2b435cd4f83..0000000000000000000000000000000000000000
--- a/ucamlookup/ibisclient/cacerts.txt
+++ /dev/null
@@ -1,89 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
-MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
-IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
-MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
-FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
-bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
-dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
-H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
-uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
-mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
-a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
-E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
-WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
-VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
-Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
-cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
-IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
-AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
-YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
-6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
-Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
-c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
-mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x
-GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv
-b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV
-BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W
-YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa
-GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg
-Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J
-WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB
-rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp
-+ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1
-ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i
-Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz
-PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og
-/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH
-oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI
-yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud
-EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2
-A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL
-MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT
-ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f
-BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn
-g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl
-fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K
-WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha
-B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc
-hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR
-TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD
-mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z
-ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y
-4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza
-8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u
------END CERTIFICATE-----
------BEGIN CERTIFICATE-----
-MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL
-BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc
-BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00
-MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM
-aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG
-SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf
-qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW
-n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym
-c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+
-O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1
-o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j
-IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq
-IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz
-8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh
-vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l
-7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG
-cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
-BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD
-ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66
-AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC
-roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga
-W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n
-lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE
-+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV
-csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd
-dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg
-KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM
-HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4
-WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M
------END CERTIFICATE-----
diff --git a/ucamlookup/ibisclient/connection.py b/ucamlookup/ibisclient/connection.py
deleted file mode 100644
index 33a69eac6e5ec082b00b83f8d76097d431661b02..0000000000000000000000000000000000000000
--- a/ucamlookup/ibisclient/connection.py
+++ /dev/null
@@ -1,414 +0,0 @@
-# --------------------------------------------------------------------------
-# Copyright (c) 2012, University of Cambridge Computing Service
-#
-# This file is part of the Lookup/Ibis client library.
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
-# License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library.  If not, see <http://www.gnu.org/licenses/>.
-#
-# Author: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-# --------------------------------------------------------------------------
-
-"""
-Connection classes to connect to the Lookup/Ibis web service and allow API
-methods to be invoked.
-"""
-
-import sys
-if sys.version_info >= (3, 0):
-    PY3 = True
-else:
-    PY3 = False
-import base64
-from datetime import date
-if PY3:
-    from http.client import HTTPSConnection
-else:
-    from httplib import HTTPSConnection
-import socket
-import os
-if PY3:
-    import urllib.parse
-else:
-    import urllib
-from .dto import IbisDto, IbisError, IbisResult, IbisResultParser
-
-try:
-    import ssl
-    _have_ssl = True
-except ImportError:
-    print("WARNING: No SSL support - connection may be insecure")
-    _have_ssl = False
-
-# Use the latest TLS protocol supported by both the client and the server.
-# Prior to Python versions 2.7.13 and 3.6, the way to do that was by
-# specifying PROTOCOL_SSLv23. In more recent versions, that is deprecated,
-# and presumably might go away one day. Instead, the new value PROTOCOL_TLS
-# has been added, and is now the recommended way to do it. This is actually
-# the same constant (2). Note that, despite the name, this will never choose
-# insecure SSL v2 or v3 protocols, because those are disabled on the server.
-if hasattr(ssl, 'PROTOCOL_TLS'):
-    _ssl_protocol = ssl.PROTOCOL_TLS
-else:
-    _ssl_protocol = ssl.PROTOCOL_SSLv23
-
-_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
-           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
-
-class IbisException(Exception):
-    """
-    Exception thrown when a web service API method fails. This is wrapper
-    around the :any:`IbisError` object returned by the server, which contains
-    the full details of what went wrong.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, error):
-        Exception.__init__(self, error.message)
-        self.error = error
-
-    def get_error(self):
-        """
-        Returns the underlying error from the server.
-
-        **Returns**
-          :any:`IbisError`
-            The underlying error from the server.
-        """
-        return self.error
-
-class HTTPSValidatingConnection(HTTPSConnection):
-    """
-    Class extending the standard :py:class:`HTTPSConnection` class from
-    :any:`http.client`, so that it checks the server's certificates,
-    validating them against the specified CA certificates.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, host, port, ca_certs):
-        HTTPSConnection.__init__(self, host, port)
-        self.ca_certs = ca_certs
-
-    def connect(self):
-        """
-        Overridden connect() method to wrap the socket using an SSLSocket,
-        and check the server certificates.
-        """
-        try:
-            self.sock = socket.create_connection((self.host, self.port))
-        except AttributeError:
-            HTTPSConnection.connect(self)
-
-        if not _have_ssl:
-            # No SSL available - insecure connection
-            print("WARNING: No SSL support - connection may be insecure")
-        elif self.ca_certs:
-            # Wrap the socket in an SSLSocket, and tell it to validate
-            # the server certificates. Note that this does not check that
-            # the certificate's host matches, so we must do that ourselves.
-            self.sock = ssl.wrap_socket(self.sock,
-                                        ca_certs = self.ca_certs,
-                                        cert_reqs = ssl.CERT_REQUIRED,
-                                        ssl_version = _ssl_protocol)
-
-            cert = self.sock.getpeercert()
-            cert_hosts = []
-            host_valid = False
-
-            if "subject" in cert:
-                for x in cert["subject"]:
-                    if x[0][0] == "commonName":
-                        cert_hosts.append(x[0][1])
-            if "subjectAltName" in cert:
-                for x in cert["subjectAltName"]:
-                    if x[0] == "dns":
-                        cert_hosts.append(x[1])
-
-            for cert_host in cert_hosts:
-                if self.host.startswith(cert_host):
-                    host_valid = True
-
-            if not host_valid:
-                raise ssl.SSLError("Host name '%s' doesn't match "\
-                                   "certificate host %s"\
-                                   % (self.host, str(cert_hosts)))
-        else:
-            # No CA certificates supplied, so can't validate the server
-            # certificates, but we still wrap the socket in an SSLSocket
-            # so that all data is encrypted.
-            self.sock = ssl.wrap_socket(self.sock,
-                                        ca_certs = None,
-                                        cert_reqs = ssl.CERT_NONE,
-                                        ssl_version = _ssl_protocol)
-
-class IbisClientConnection:
-    """
-    Class to connect to the Lookup/Ibis server and invoke web service API
-    methods.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, host, port, url_base, check_certs):
-        self.host = host
-        self.port = port
-        self.url_base = url_base
-
-        if not self.url_base.startswith("/"):
-            self.url_base = "/%s" % self.url_base
-        if not self.url_base.endswith("/"):
-            self.url_base = "%s/" % self.url_base
-
-        if check_certs:
-            ibisclient_dir = os.path.realpath(os.path.dirname(__file__))
-            self.ca_certs = os.path.join(ibisclient_dir, "cacerts.txt")
-        else:
-            self.ca_certs = None
-
-        self.username = None
-        self.password = None
-        self.set_username("anonymous")
-
-    def _update_authorization(self):
-        credentials = "%s:%s" % (self.username, self.password)
-        if PY3:
-            credential_bytes = bytes(credentials, "UTF-8")
-            base64_credentials = str(base64.b64encode(credential_bytes), "UTF-8")
-            self.authorization = "Basic %s" % base64_credentials
-        else:
-            self.authorization = "Basic %s" % base64.b64encode(credentials)
-
-    def set_username(self, username):
-        """
-        Set the username to use when connecting to the Lookup/Ibis web
-        service. By default connections are anonymous, which gives read-only
-        access. This method enables authentication as a group, using the
-        group's password, which gives read/write access and also access to
-        certain non-public data, based on the group's privileges.
-
-        This method may be called at any time, and affects all subsequent
-        access using this connection, but does not affect any other
-        :any:`IbisClientConnection` objects.
-
-        **Parameters**
-          `username` : str
-            [required] The username to connect as. This should either be
-            ``"anonymous"`` (the default) or the name of a group.
-        """
-        self.username = username
-        self._update_authorization()
-
-    def set_password(self, password):
-        """
-        Set the password to use when connecting to the Lookup/Ibis web
-        service. This is only necessary when connecting as a group, in
-        which case it should be that group's password.
-
-        **Parameters**
-          `password` : str
-            [required] The group password.
-        """
-        self.password = password
-        self._update_authorization()
-
-    def _params_to_strings(self, params):
-        """
-        Convert the values in a parameter map into strings suitable for
-        sending to the server. Any null values will be omitted.
-        """
-        new_params = {}
-        if PY3:
-            items = params.items()
-        else:
-            items = params.iteritems()
-        for key, value in items:
-            if value != None:
-                if isinstance(value, bool):
-                    if value: new_params[key] = "true"
-                    else: new_params[key] = "false"
-                elif isinstance(value, date):
-                    new_params[key] = "%02d %s %d" % (value.day,
-                                                      _MONTHS[value.month-1],
-                                                      value.year)
-                elif isinstance(value, list) or isinstance(value, tuple):
-                    new_params[key] = ",".join(value)
-                elif isinstance(value, IbisDto):
-                    new_params[key] = value.encoded_string()
-                elif not isinstance(value, str):
-                    new_params[key] = str(value)
-                else:
-                    new_params[key] = value
-
-        return new_params
-
-    def _build_url(self, path, path_params={}, query_params={}):
-        """
-        Build the full URL needed to invoke a method in the web service API.
-
-        The path may contain standard Python format specifiers, which will
-        be substituted from the path parameters (suitably URL-encoded). Thus
-        for example, given the following arguments:
-
-            * path = "api/v1/person/%(scheme)s/%(identifier)s"
-            * path_params = {"scheme": "crsid", "identifier": "dar17"}
-            * query_params = {"fetch": "email,title"}
-
-        this method will create a URL like the following:
-
-            api/v1/person/crsid/dar17?fetch=email%2Ctitle
-
-        Note that all parameter values are automatically URL-encoded.
-        """
-        if PY3:
-            for key, value in path_params.items():
-                path_params[key] = urllib.parse.quote_plus(value)
-        else:
-            for key, value in path_params.iteritems():
-                path_params[key] = urllib.quote_plus(value)
-        path = path % path_params
-
-
-        if PY3:
-            if "flatten" not in query_params:
-                query_params["flatten"] = "true"
-            path += "?%s" % urllib.parse.urlencode(query_params)
-        else:
-            if not query_params.has_key("flatten"):
-                query_params["flatten"] = "true"
-            path += "?%s" % urllib.urlencode(query_params)
-
-        if path.startswith("/"):
-            return "%s%s" % (self.url_base, path[1:])
-        return "%s%s" % (self.url_base, path)
-
-    def invoke_method(self, method, path, path_params={},
-                      query_params={}, form_params={}):
-        """
-        Invoke a web service GET, POST, PUT or DELETE method.
-
-        The path should be the relative path to the method with standard
-        Python format specifiers for any path parameters, for example
-        ``"/api/v1/person/%(scheme)s/%(identifier)s"``. Any path parameters
-        specified are then substituted into the path.
-
-        **Parameters**
-          `method` : str
-            [required] The method type (``"GET"``, ``"POST"``, ``"PUT"`` or
-            ``"DELETE"``).
-
-          `path` : str
-            [required] The path to the method to invoke.
-
-          `path_params` : dict
-            [optional] Any path parameters that should be inserted into the
-            path in place of any format specifiers.
-
-          `query_params` : dict
-            [optional] Any query parameters to add as part of the URL's query
-            string.
-
-          `form_params` : dict
-            [optional] Any form parameters to submit.
-
-        **Returns**
-          :any:`IbisResult`
-            The result of invoking the method.
-        """
-        path_params = self._params_to_strings(path_params)
-        query_params = self._params_to_strings(query_params)
-        form_params = self._params_to_strings(form_params)
-
-        conn = HTTPSValidatingConnection(self.host, self.port, self.ca_certs)
-        url = self._build_url(path, path_params, query_params)
-        headers = {"Accept": "application/xml",
-                   "Authorization": self.authorization}
-
-        if form_params:
-            if PY3:
-                body = urllib.parse.urlencode(form_params)
-            else:
-                body = urllib.urlencode(form_params)
-            conn.request(method, url, body, headers)
-        else:
-            conn.request(method, url, headers=headers)
-
-        response = conn.getresponse()
-        content_type = response.getheader("Content-type")
-        if content_type != "application/xml":
-            error = IbisError({"status": response.status,
-                               "code": response.reason})
-            error.message = "Unexpected result from server"
-            error.details = response.read()
-
-            result = IbisResult()
-            result.error = error
-
-            return result
-
-        parser = IbisResultParser()
-        result = parser.parse_xml(response.read())
-        conn.close()
-
-        return result
-
-def createConnection():
-    """
-    Create an IbisClientConnection to the Lookup/Ibis web service API at
-    https://www.lookup.cam.ac.uk/.
-
-    The connection is initially anonymous, but this may be changed using
-    its :any:`set_username() <IbisClientConnection.set_username>` and
-    :any:`set_password() <IbisClientConnection.set_password>` methods.
-
-    **Returns**
-      :any:`IbisClientConnection`
-        A new connection to the Lookup server.
-    """
-    return IbisClientConnection("www.lookup.cam.ac.uk", 443, "", True)
-
-def createTestConnection():
-    """
-    Create an IbisClientConnection to the Lookup/Ibis test web service API
-    at https://lookup-test.csx.cam.ac.uk/.
-
-    The connection is initially anonymous, but this may be changed using
-    its :any:`set_username() <IbisClientConnection.set_username>` and
-    :any:`set_password() <IbisClientConnection.set_password>` methods.
-
-    .. note::
-      This test server is not guaranteed to always be available, and the data
-      on it may be out of sync with the data on the live system.
-
-    **Returns**
-      :any:`IbisClientConnection`
-        A new connection to the Lookup test server.
-    """
-    return IbisClientConnection("lookup-test.csx.cam.ac.uk", 443, "", True)
-
-def createLocalConnection():
-    """
-    Create an IbisClientConnection to a Lookup/Ibis web service API running
-    locally on https://localhost:8443/ibis/.
-
-    The connection is initially anonymous, but this may be changed using
-    its :any:`set_username() <IbisClientConnection.set_username>` and
-    :any:`set_password() <IbisClientConnection.set_password>` methods.
-
-    This is intended for testing during development. The local server is
-    assumed to be using self-signed certificates, which will not be checked.
-
-    **Returns**
-      :any:`IbisClientConnection`
-        A new connection to a Lookup server assumed to be running on
-        localhost.
-    """
-    return IbisClientConnection("localhost", 8443, "ibis", False)
diff --git a/ucamlookup/ibisclient/dto.py b/ucamlookup/ibisclient/dto.py
deleted file mode 100644
index 62acb3d881f8d4d156d88d79e6041e21373a2845..0000000000000000000000000000000000000000
--- a/ucamlookup/ibisclient/dto.py
+++ /dev/null
@@ -1,1432 +0,0 @@
-# --------------------------------------------------------------------------
-# Copyright (c) 2012, University of Cambridge Computing Service
-#
-# This file is part of the Lookup/Ibis client library.
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
-# License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library.  If not, see <http://www.gnu.org/licenses/>.
-#
-# Author: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-# --------------------------------------------------------------------------
-
-"""
-DTO classes for transferring data from the server to client in the web
-service API.
-
-All web service API methods return an instance or a list of one of these DTO
-classes, or a primitive type such as a bool, int or string.
-
-In the case of an error, an :any:`IbisException` will be raised which will
-contain an instance of an :any:`IbisError` DTO.
-"""
-
-import sys
-if sys.version_info >= (3, 0):
-    PY3 = True
-else:
-    PY3 = False
-import base64
-from datetime import date
-from xml.parsers import expat
-
-import sys
-if sys.hexversion < 0x02040000:
-    from sets import Set as set
-
-_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
-           "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
-
-class IbisDto(object):
-    """
-    Base class for all DTO classes.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    # All properties
-    __slots__ = []
-
-    # Properties marked as @XmlAttribte in the JAXB class
-    xml_attrs = set()
-
-    # Properties marked as @XmlElement in the JAXB class
-    xml_elems = set()
-
-    # Properties marked as @XmlElementWrapper in the JAXB class
-    xml_arrays = set()
-
-    def __init__(self, attrs={}):
-        """
-        Create an IbisDto from the attributes of an XML node. This just
-        sets the properties marked as @XmlAttribute in the JAXB class.
-        """
-        for attr in self.__class__.__slots__:
-            setattr(self, attr, None)
-        for attr in self.__class__.xml_attrs:
-            setattr(self, attr, attrs.get(attr))
-
-    def start_child_element(self, tagname):
-        """
-        Start element callback invoked during XML parsing when the opening
-        tag of a child element is encountered. This creates and returns any
-        properties marked as @XmlElementWrapper in the JAXB class, so that
-        child collections can be populated.
-        """
-        if tagname in self.__class__.xml_arrays:
-            if getattr(self, tagname) == None: setattr(self, tagname, [])
-            return getattr(self, tagname)
-        return None
-
-    def end_child_element(self, tagname, data):
-        """
-        End element callback invoked during XML parsing when the end tag of
-        a child element is encountered, and the tag's data is available. This
-        sets the value of any properties marked as @XmlElement in the JAXB
-        class.
-        """
-        if tagname in self.__class__.xml_elems:
-            setattr(self, tagname, data)
-
-# --------------------------------------------------------------------------
-# IbisPerson: see uk.ac.cam.ucs.ibis.dto.IbisPerson.java
-# --------------------------------------------------------------------------
-class IbisPerson(IbisDto):
-    """
-    Class representing a person returned by the web service API. Note that
-    the identifier is the person's primary identifier (typically their CRSid),
-    regardless of which identifier was used to query for the person.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "cancelled":
-            """
-            bool
-              Flag indicating if the person is cancelled.
-            """,
-        "identifier":
-            """
-            :any:`IbisIdentifier`
-              The person's primary identifier (typically their CRSid).
-            """,
-        "displayName":
-            """
-            str
-              The person's display name (if visible).
-            """,
-        "registeredName":
-            """
-            str
-              The person's registered name (if visible).
-            """,
-        "surname":
-            """
-            str
-              The person's surname (if visible).
-            """,
-        "visibleName":
-            """
-            str
-              The person's display name if that is visible, otherwise their
-              registered name if that is visible, otherwise their surname if
-              that is visible, otherwise the value of their primary identifier
-              (typically their CRSid) which is always visible.
-            """,
-        "misAffiliation":
-            """
-            str
-              The person's MIS status (``"staff"``, ``"student"``,
-              ``"staff,student"`` or ``""``).
-            """,
-        "identifiers":
-            """
-            list of :any:`IbisIdentifier`
-              A list of the person's identifiers. This will only be populated
-              if the `fetch` parameter included the ``"all_identifiers"``
-              option.
-            """,
-        "attributes":
-            """
-            list of :any:`IbisAttribute`
-              A list of the person's attributes. This will only be populated
-              if the `fetch` parameter includes the ``"all_attrs"`` option, or
-              any specific attribute schemes such as ``"email"`` or
-              ``"title"``, or the special pseudo-attribute scheme
-              ``"phone_numbers"``.
-            """,
-        "institutions":
-            """
-            list of :any:`IbisInstitution`
-              A list of all the institution's to which the person belongs.
-              This will only be populated if the `fetch` parameter includes
-              the ``"all_insts"`` option.
-            """,
-        "groups":
-            """
-            list of :any:`IbisGroup`
-              A list of all the groups to which the person belongs, including
-              indirect group memberships, via groups that include other
-              groups. This will only be populated if the `fetch` parameter
-              includes the ``"all_groups"`` option.
-            """,
-        "directGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of all the groups that the person directly belongs to.
-              This does not include indirect group memberships - i.e., groups
-              that include these groups. This will only be populated if the
-              `fetch` parameter includes the ``"direct_groups"`` option.
-            """,
-        "id":
-            """
-            str
-              An ID that can uniquely identify this person within the returned
-              XML/JSON document. This is only used in the flattened XML/JSON
-              representation (if the "flatten" parameter is specified).
-            """,
-        "ref":
-            """
-            str
-              A reference (by id) to a person element in the XML/JSON
-              document. This is only used in the flattened XML/JSON
-              representation (if the "flatten" parameter is specified).
-            """,
-        "unflattened":
-            """
-            bool
-              Flag to prevent infinite recursion due to circular references.
-            """
-    }
-
-    xml_attrs = set(["cancelled", "id", "ref"])
-
-    xml_elems = set(["identifier", "displayName", "registeredName",
-                     "surname", "visibleName", "misAffiliation"])
-
-    xml_arrays = set(["identifiers", "attributes", "institutions",
-                      "groups", "directGroups"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisPerson from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.cancelled != None:
-            self.cancelled = self.cancelled.lower() == "true"
-        self.unflattened = False
-
-    def is_staff(self):
-        """
-        Returns :any:`True` if the person is a member of staff.
-
-        Note that this tests for an misAffiliation of ``""``, ``"staff"`` or
-        ``"staff,student"`` since some members of staff will have a blank
-        misAffiliation.
-        """
-        return self.misAffiliation == None or\
-               self.misAffiliation != "student";
-
-    def is_student(self):
-        """
-        Returns :any:`True` if the person is a student.
-
-        This tests for an misAffiliation of ``"student"`` or
-        ``"staff,student"``.
-        """
-        return self.misAffiliation != None and\
-               self.misAffiliation.find("student") != -1;
-
-    def unflatten(self, em):
-        """ Unflatten a single IbisPerson. """
-        if self.ref:
-            person = em.get_person(self.ref)
-            if not person.unflattened:
-                person.unflattened = True
-                unflatten_insts(em, person.institutions)
-                unflatten_groups(em, person.groups)
-                unflatten_groups(em, person.directGroups)
-            return person
-        return self
-
-def unflatten_people(em, people):
-    """ Unflatten a list of IbisPerson objects (done in place). """
-    if people:
-        for idx, person in enumerate(people):
-            people[idx] = person.unflatten(em)
-
-# --------------------------------------------------------------------------
-# IbisInstitution: see uk.ac.cam.ucs.ibis.dto.IbisInstitution.java
-# --------------------------------------------------------------------------
-class IbisInstitution(IbisDto):
-    """
-    Class representing an institution returned by the web service API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "cancelled":
-            """
-            bool
-              Flag indicating if the institution is cancelled.
-            """,
-        "instid":
-            """
-            str
-              The institution's unique ID (e.g., ``"CS"``).
-            """,
-        "name":
-            """
-            str
-              The institution's name.
-            """,
-        "acronym":
-            """
-            str
-              The institution's acronym, if set (e.g., ``"UCS"``).
-            """,
-        "attributes":
-            """
-            list of :any:`IbisAttribute`
-              A list of the institution's attributes. This will only be
-              populated if the `fetch` parameter includes the ``"all_attrs"``
-              option, or any specific attribute schemes such as ``"email"`` or
-              ``"address"``, or the special pseudo-attribute scheme
-              ``"phone_numbers"``.
-            """,
-        "contactRows":
-            """
-            list of :any:`IbisContactRow`
-              A list of the institution's contact rows. This will only be
-              populated if the `fetch` parameter includes the
-              ``"contact_rows"`` option.
-            """,
-        "members":
-            """
-            list of :any:`IbisPerson`
-              A list of the institution's members. This will only be populated
-              if the `fetch` parameter includes the ``"all_members"`` option.
-            """,
-        "parentInsts":
-            """
-            list of :any:`IbisInstitution`
-              A list of the institution's parent institutions. This will only
-              be populated if the `fetch` parameter includes the
-              ``"parent_insts"`` option.
-
-              .. note::
-                Currently all institutions have one parent, but in the future
-                institutions may have multiple parents.
-            """,
-        "childInsts":
-            """
-            list of :any:`IbisInstitution`
-              A list of the institution's child institutions. This will only
-              be populated if the `fetch` parameter includes the
-              ``"child_insts"`` option.
-            """,
-        "groups":
-            """
-            list of :any:`IbisGroup`
-              A list of all the groups that belong to the institution. This
-              will only be populated if the `fetch` parameter includes the
-              ``"inst_groups"`` option.
-            """,
-        "membersGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that form the institution's membership.
-              This will only be populated if the `fetch` parameter includes
-              the ``"members_groups"`` option.
-            """,
-        "managedByGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that manage this institution. This will
-              only be populated if the `fetch` parameter includes the
-              ``"managed_by_groups"`` option.
-            """,
-        "id":
-            """
-            str
-              An ID that can uniquely identify this institution within the
-              returned XML/JSON document. This is only used in the flattened
-              XML/JSON representation (if the "flatten" parameter is
-              specified).
-            """,
-        "ref":
-            """
-            str
-              A reference (by id) to an institution element in the XML/JSON
-              document. This is only used in the flattened XML/JSON
-              representation (if the "flatten" parameter is specified).
-            """,
-        "unflattened":
-            """
-            bool
-              Flag to prevent infinite recursion due to circular references.
-            """
-    }
-
-    xml_attrs = set(["cancelled", "instid", "id", "ref"])
-
-    xml_elems = set(["name", "acronym"])
-
-    xml_arrays = set(["attributes", "contactRows", "members",
-                      "parentInsts", "childInsts", "groups",
-                      "membersGroups", "managedByGroups"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisInstitution from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.cancelled != None:
-            self.cancelled = self.cancelled.lower() == "true"
-        self.unflattened = False
-
-    def unflatten(self, em):
-        """ Unflatten a single IbisInstitution. """
-        if self.ref:
-            inst = em.get_institution(self.ref)
-            if not inst.unflattened:
-                inst.unflattened = True
-                unflatten_contact_rows(em, inst.contactRows)
-                unflatten_people(em, inst.members)
-                unflatten_insts(em, inst.parentInsts)
-                unflatten_insts(em, inst.childInsts)
-                unflatten_groups(em, inst.groups)
-                unflatten_groups(em, inst.membersGroups)
-                unflatten_groups(em, inst.managedByGroups)
-            return inst
-        return self
-
-def unflatten_insts(em, insts):
-    """ Unflatten a list of IbisInstitution objects (done in place). """
-    if insts:
-        for idx, inst in enumerate(insts):
-            insts[idx] = inst.unflatten(em)
-
-# --------------------------------------------------------------------------
-# IbisGroup: see uk.ac.cam.ucs.ibis.dto.IbisGroup.java
-# --------------------------------------------------------------------------
-class IbisGroup(IbisDto):
-    """
-    Class representing a group returned by the web service API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "cancelled":
-            """
-            bool
-              Flag indicating if the group is cancelled.
-            """,
-        "groupid":
-            """
-            str
-              The group's numeric ID (actually a string e.g., ``"100656"``).
-            """,
-        "name":
-            """
-            str
-              The group's unique name (e.g., ``"cs-editors"``).
-            """,
-        "title":
-            """
-            str
-              The group's title.
-            """,
-        "description":
-            """
-            str
-              The more detailed description of the group.
-            """,
-        "email":
-            """
-            str
-              The group's email address.
-            """,
-        "membersOfInst":
-            """
-            :any:`IbisInstitution`
-              The details of the institution for which this group forms all
-              or part of the membership. This will only be set for groups that
-              are membership groups of institutions if the `fetch` parameter
-              includes the ``"members_of_inst"`` option.
-            """,
-        "members":
-            """
-            list of :any:`IbisPerson`
-              A list of the group's members, including (recursively) any
-              members of any included groups. This will only be populated if
-              the `fetch` parameter includes the ``"all_members"`` option.
-            """,
-        "directMembers":
-            """
-            list of :any:`IbisPerson`
-              A list of the group's direct members, not including any members
-              included via groups included by this group. This will only be
-              populated if the `fetch` parameter includes the
-              ``"direct_members"`` option.
-            """,
-        "owningInsts":
-            """
-            list of :any:`IbisInstitution`
-              A list of the institutions to which this group belongs. This
-              will only be populated if the `fetch` parameter includes the
-              ``"owning_insts"`` option.
-            """,
-        "managesInsts":
-            """
-            list of :any:`IbisInstitution`
-              A list of the institutions managed by this group. This will only
-              be populated if the `fetch` parameter includes the
-              ``"manages_insts"`` option.
-            """,
-        "managesGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups managed by this group. This will only be
-              populated if the `fetch` parameter includes the
-              ``"manages_groups"`` option.
-            """,
-        "managedByGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that manage this group. This will only be
-              populated if the `fetch` parameter includes the
-              ``"managed_by_groups"`` option.
-            """,
-        "readsGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that this group has privileged access to.
-              Members of this group will be able to read the members of any of
-              those groups, regardless of the membership visibilities. This
-              will only be populated if the `fetch` parameter includes the
-              ``"reads_groups"`` option.
-            """,
-        "readByGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that have privileged access to this group.
-              Members of those groups will be able to read the members of this
-              group, regardless of the membership visibilities. This will only
-              be populated if the `fetch` parameter includes the
-              ``"read_by_groups"`` option.
-            """,
-        "includesGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups directly included in this group. Any
-              members of the included groups (and recursively any groups that
-              they include) will automatically be included in this group. This
-              will only be populated if the `fetch` parameter includes the
-              ``"includes_groups"`` option.
-            """,
-        "includedByGroups":
-            """
-            list of :any:`IbisGroup`
-              A list of the groups that directly include this group. Any
-              members of this group will automatically be included in those
-              groups (and recursively in any groups that include those
-              groups). This will only be populated if the `fetch` parameter
-              includes the ``"included_by_groups"`` option.
-            """,
-        "id":
-            """
-            str
-              An ID that can uniquely identify this group within the returned
-              XML/JSON document. This is only used in the flattened XML/JSON
-              representation (if the "flatten" parameter is specified).
-            """,
-        "ref":
-            """
-            str
-              A reference (by id) to a group element in the XML/JSON document.
-              This is only used in the flattened XML/JSON representation (if
-              the "flatten" parameter is specified).
-            """,
-        "unflattened":
-            """
-            bool
-              Flag to prevent infinite recursion due to circular references.
-            """
-    }
-
-    xml_attrs = set(["cancelled", "groupid", "id", "ref"])
-
-    xml_elems = set(["name", "title", "description", "emails",
-                     "membersOfInst"])
-
-    xml_arrays = set(["members", "directMembers",
-                      "owningInsts", "managesInsts",
-                      "managesGroups", "managedByGroups",
-                      "readsGroups", "readByGroups",
-                      "includesGroups", "includedByGroups"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisGroup from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.cancelled != None:
-            self.cancelled = self.cancelled.lower() == "true"
-        self.unflattened = False
-
-    def unflatten(self, em):
-        """ Unflatten a single IbisGroup. """
-        if self.ref:
-            group = em.get_group(self.ref)
-            if not group.unflattened:
-                group.unflattened = True
-                if group.membersOfInst:
-                    group.membersOfInst = group.membersOfInst.unflatten(em)
-                unflatten_people(em, group.members)
-                unflatten_people(em, group.directMembers)
-                unflatten_insts(em, group.owningInsts)
-                unflatten_insts(em, group.managesInsts)
-                unflatten_groups(em, group.managesGroups)
-                unflatten_groups(em, group.managedByGroups)
-                unflatten_groups(em, group.readsGroups)
-                unflatten_groups(em, group.readByGroups)
-                unflatten_groups(em, group.includesGroups)
-                unflatten_groups(em, group.includedByGroups)
-            return group
-        return self
-
-def unflatten_groups(em, groups):
-    """ Unflatten a list of IbisGroup objects (done in place). """
-    if groups:
-        for idx, group in enumerate(groups):
-            groups[idx] = group.unflatten(em)
-
-# --------------------------------------------------------------------------
-# IbisIdentifier: see uk.ac.cam.ucs.ibis.dto.IbisIdentifier.java
-# --------------------------------------------------------------------------
-class IbisIdentifier(IbisDto):
-    """
-    Class representing a person's identifier, for use by the web service
-    API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "scheme":
-            """
-            str
-              The identifier's scheme (e.g., ``"crsid"``).
-            """,
-        "value":
-            """
-            str
-              The identifier's value in that scheme (e.g., a specific CRSid
-              value).
-            """
-    }
-
-    xml_attrs = set(["scheme"])
-
-# --------------------------------------------------------------------------
-# IbisAttribute: see uk.ac.cam.ucs.ibis.dto.IbisAttribute.java
-# --------------------------------------------------------------------------
-class IbisAttribute(IbisDto):
-    """
-    Class representing an attribute of a person or institution returned by
-    the web service API. Note that for institution attributes, the
-    :any:`instid <IbisAttribute.instid>`,
-    :any:`visibility <IbisAttribute.visibility>` and
-    :any:`owningGroupid <IbisAttribute.owningGroupid>` fields will be
-    :any:`None`.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "attrid":
-            """
-            int
-              The unique internal identifier of the attribute.
-            """,
-        "scheme":
-            """
-            str
-              The attribute's scheme.
-            """,
-        "value":
-            """
-            str
-              The attribute's value (except for binary attributes).
-            """,
-        "binaryData":
-            """
-            bytes
-              The binary data held in the attribute (e.g., a JPEG photo).
-            """,
-        "comment":
-            """
-            str
-              Any comment associated with the attribute.
-            """,
-        "instid":
-            """
-            str
-              For a person attribute, the optional institution that the
-              attribute is associated with. This will not be set for
-              institution attributes.
-            """,
-        "visibility":
-            """
-            str
-              For a person attribute, it's visibility (``"private"``,
-              ``"institution"``, ``"university"`` or ``"world"``). This will
-              not be set for institution attributes.
-            """,
-        "effectiveFrom":
-            """
-            date
-              For time-limited attributes, the date from which it takes
-              effect.
-            """,
-        "effectiveTo":
-            """
-            date
-              For time-limited attributes, the date after which it is no
-              longer effective.
-            """,
-        "owningGroupid":
-            """
-            str
-              For a person attribute, the ID of the group that owns it
-              (typically the user agent group that created it).
-            """
-    }
-
-    xml_attrs = set(["attrid", "scheme", "instid", "visibility",
-                     "effectiveFrom", "effectiveTo", "owningGroupid"])
-
-    xml_elems = set(["value", "binaryData", "comment"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisAttribute from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.attrid != None:
-            self.attrid = int(self.attrid)
-        if self.effectiveFrom != None:
-            self.effectiveFrom = parse_date(self.effectiveFrom)
-        if self.effectiveTo != None:
-            self.effectiveTo = parse_date(self.effectiveTo)
-
-    def end_child_element(self, tagname, data):
-        """
-        Overridden end element callback to decode binary data.
-        """
-        IbisDto.end_child_element(self, tagname, data)
-        if tagname == "binaryData" and self.binaryData != None:
-            self.binaryData = base64.b64decode(self.binaryData)
-
-    def encoded_string(self):
-        """
-        Encode this attribute as an ASCII string suitable for passing as a
-        parameter to a web service API method. This string is compatible with
-        `valueOf(java.lang.String)` on the corresponding Java class,
-        used on the server to decode the attribute parameter.
-
-        .. note::
-          This requires that the attribute's
-          :any:`scheme <IbisAttribute.scheme>` field be set, and typically the
-          :any:`value <IbisAttribute.value>` or
-          :any:`binaryData <IbisAttribute.binaryData>` should also be set.
-
-        **Returns**
-          str
-            The string encoding of this attribute.
-        """
-        if not self.scheme:
-            raise ValueError("Attribute scheme must be set")
-
-        result = "scheme:%s" % base64.b64encode(self.scheme)
-        if self.attrid != None:
-            result = "%s,attrid:%d" % (result, self.attrid)
-        if self.value != None:
-            result = "%s,value:%s" % (result, base64.b64encode(self.value))
-        if self.binaryData != None:
-            result = "%s,binaryData:%s" %\
-                     (result, base64.b64encode(self.binaryData))
-        if self.comment != None:
-            result = "%s,comment:%s" %\
-                     (result, base64.b64encode(self.comment))
-        if self.instid != None:
-            result = "%s,instid:%s" %\
-                     (result, base64.b64encode(self.instid))
-        if self.visibility != None:
-            result = "%s,visibility:%s" %\
-                     (result, base64.b64encode(self.visibility))
-        if self.effectiveFrom != None:
-            result = "%s,effectiveFrom:%02d %s %d" %\
-                     (result,
-                      self.effectiveFrom.day,
-                      _MONTHS[self.effectiveFrom.month-1],
-                      self.effectiveFrom.year)
-        if self.effectiveTo != None:
-            result = "%s,effectiveTo:%02d %s %d" %\
-                     (result,
-                      self.effectiveTo.day,
-                      _MONTHS[self.effectiveTo.month-1],
-                      self.effectiveTo.year)
-        if self.owningGroupid != None:
-            result = "%s,owningGroupid:%s" %\
-                     (result, base64.b64encode(self.owningGroupid))
-        return result
-
-def parse_date(s):
-    """ Parse a date string from XML. """
-    s = s.strip()
-    return date(int(s[:4]), int(s[5:7]), int(s[8:10]))
-
-# --------------------------------------------------------------------------
-# IbisError: see uk.ac.cam.ucs.ibis.dto.IbisError.java
-# --------------------------------------------------------------------------
-class IbisError(IbisDto):
-    """
-    Class representing an error returned by the web service API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "status":
-            """
-            int
-              The HTTP error status code.
-            """,
-        "code":
-            """
-            str
-              A short textual description of the error status code.
-            """,
-        "message":
-            """
-            str
-              A short textual description of the error message (typically one
-              line).
-            """,
-        "details":
-            """
-            str
-              The full details of the error (e.g., a Java stack trace).
-            """
-    }
-
-    xml_attrs = set(["status"])
-
-    xml_elems = set(["code", "message", "details"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisError from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.status != None:
-            self.status = int(self.status)
-
-# --------------------------------------------------------------------------
-# IbisAttributeScheme: see uk.ac.cam.ucs.ibis.dto.IbisAttributeScheme.java
-# --------------------------------------------------------------------------
-class IbisAttributeScheme(IbisDto):
-    """
-    Class representing an attribute scheme. This may apply to attributes of
-    people or institutions.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "schemeid":
-            """
-            str
-              The unique identifier of the attribute scheme.
-            """,
-        "precedence":
-            """
-            int
-              The attribute scheme's precedence. Methods that return or
-              display attributes sort the results primarily in order of
-              increasing values of attribute scheme precedence.
-            """,
-        "ldapName":
-            """
-            str
-              The name of the attribute scheme in LDAP, if it is exported to
-              LDAP. Note that many attributes are not exported to LDAP, in
-              which case this name is typically just equal to the scheme's ID.
-            """,
-        "displayName":
-            """
-            str
-              The display name for labelling attributes in this scheme.
-            """,
-        "dataType":
-            """
-            str
-              The attribute scheme's datatype.
-            """,
-        "multiValued":
-            """
-            bool
-              Flag indicating whether attributes in this scheme can be
-              multi-valued.
-            """,
-        "multiLined":
-            """
-            bool
-              Flag for textual attributes schemes indicating whether they are
-              multi-lined.
-            """,
-        "searchable":
-            """
-            bool
-              Flag indicating whether attributes of this scheme are searched
-              by the default search functionality.
-            """,
-        "regexp":
-            """
-            str
-              For textual attributes, an optional regular expression that all
-              attributes in this scheme match.
-            """
-    }
-
-    xml_attrs = set(["schemeid", "precedence", "multiValued", "multiLined",
-                     "searchable"])
-
-    xml_elems = set(["ldapName", "displayName", "dataType", "regexp"])
-
-    def __init__(self, attrs={}):
-        """
-        Create an IbisAttributeScheme from the attributes of an XML node.
-        """
-        IbisDto.__init__(self, attrs)
-        if self.precedence != None:
-            self.precedence = int(self.precedence)
-        if self.multiValued != None:
-            self.multiValued = self.multiValued.lower() == "true"
-        if self.multiLined != None:
-            self.multiLined = self.multiLined.lower() == "true"
-        if self.searchable != None:
-            self.searchable = self.searchable.lower() == "true"
-
-# --------------------------------------------------------------------------
-# IbisContactRow: see uk.ac.cam.ucs.ibis.dto.IbisContactRow.java
-# --------------------------------------------------------------------------
-class IbisContactRow(IbisDto):
-    """
-    Class representing an institution contact row, for use by the web
-    services API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "description":
-            """
-            str
-              The contact row's text.
-            """,
-        "bold":
-            """
-            bool
-              Flag indicating if the contact row's text is normally displayed
-              in bold.
-            """,
-        "italic":
-            """
-            bool
-              Flag indicating if the contact row's text is normally displayed
-              in italics.
-            """,
-        "addresses":
-            """
-            list of str
-              A list of the contact row's addresses. This will never be
-              :any:`None`, but it may be an empty list.
-            """,
-        "emails":
-            """
-            list of str
-              A list of the contact row's email addresses. This will never be
-              :any:`None`, but it may be an empty list.
-            """,
-        "people":
-            """
-            list of :any:`IbisPerson`
-              A list of the people referred to by the contact row. This will
-              never by :any:`None`, but it may be an empty list.
-            """,
-        "phoneNumbers":
-            """
-            list of :any:`IbisContactPhoneNumber`
-              A list of the contact row's phone numbers. This will never be
-              :any:`None`, but it may be an empty list.
-            """,
-        "webPages":
-            """
-            list of :any:`IbisContactWebPage`
-              A list of the contact row's web pages. This will never be
-              :any:`None`, but it may be an empty list.
-            """,
-        "unflattened":
-            """
-            bool
-              Flag to prevent infinite recursion due to circular references.
-            """
-    }
-
-    xml_attrs = set(["bold", "italic"])
-
-    xml_elems = set(["description"])
-
-    xml_arrays = set(["addresses", "emails", "people", "phoneNumbers",
-                      "webPages"])
-
-    def __init__(self, attrs={}):
-        """ Create an IbisContactRow from the attributes of an XML node. """
-        IbisDto.__init__(self, attrs)
-        if self.bold != None:
-            self.bold = self.bold.lower() == "true"
-        if self.italic != None:
-            self.italic = self.italic.lower() == "true"
-        self.unflattened = False
-
-    def unflatten(self, em):
-        """ Unflatten a single IbisContactRow. """
-        if not self.unflattened:
-            self.unflattened = True
-            unflatten_people(em, self.people)
-        return self
-
-def unflatten_contact_rows(em, contact_rows):
-    """ Unflatten a list of IbisContactRow objects (done in place). """
-    if contact_rows:
-        for idx, contact_row in enumerate(contact_rows):
-            contact_rows[idx] = contact_row.unflatten(em)
-
-# --------------------------------------------------------------------------
-# IbisContactPhoneNumber:
-#     see uk.ac.cam.ucs.ibis.dto.IbisContactPhoneNumber.java
-# --------------------------------------------------------------------------
-class IbisContactPhoneNumber(IbisDto):
-    """
-    Class representing a phone number held on an institution contact row, for
-    use by the web service API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "phoneType":
-            """
-            str
-              The phone number's type.
-            """,
-        "number":
-            """
-            str
-              The phone number.
-            """,
-        "comment":
-            """
-            str
-              Any comment associated with the phone number.
-            """
-    }
-
-    xml_attrs = set(["phoneType"])
-
-    xml_elems = set(["number", "comment"])
-
-# --------------------------------------------------------------------------
-# IbisContactWebPage: see uk.ac.cam.ucs.ibis.dto.IbisContactWebPage.java
-# --------------------------------------------------------------------------
-class IbisContactWebPage(IbisDto):
-    """
-    Class representing a web page referred to by an institution contact row,
-    for use by the web service API.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "url":
-            """
-            str
-              The web page's URL.
-            """,
-        "label":
-            """
-            str
-              The web page's label (link text) if set.
-            """
-    }
-
-    xml_elems = set(["url", "label"])
-
-# --------------------------------------------------------------------------
-# IbisResult: see uk.ac.cam.ucs.ibis.dto.IbisResult.java
-# --------------------------------------------------------------------------
-class IbisResult(IbisDto):
-    """
-    Class representing the top-level container for all results.
-
-    This may be just a simple textual value or it may contain more complex
-    entities such as people, institutions, groups, attributes, etc.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    __slots__ = {
-        "version":
-            """
-            str
-              The web service API version number.
-            """,
-        "value":
-            """
-            str
-              The value returned by methods that return a simple textual
-              value.
-            """,
-        "person":
-            """
-            :any:`IbisPerson`
-              The person returned by methods that return a single person.
-
-              Note that methods that may return multiple people will always
-              use the :any:`people <IbisResult.people>` field, even if only
-              one person was returned.
-            """,
-        "institution":
-            """
-            :any:`IbisInstitution`
-              The institution returned by methods that return a single
-              institution.
-
-              Note that methods that may return multiple institutions will
-              always use the :any:`institutions <IbisResult.institutions>`
-              field, even if only one institution was returned.
-            """,
-        "group":
-            """
-            :any:`IbisGroup`
-              The group returned by methods that return a single group.
-
-              Note that methods that may return multiple groups will always
-              use the :any:`groups <IbisResult.groups>` field, even if only
-              one group was returned.
-            """,
-        "identifier":
-            """
-            :any:`IbisIdentifier`
-              The identifier returned by methods that return a single
-              identifier.
-            """,
-        "attribute":
-            """
-            :any:`IbisAttribute`
-              The person or institution attribute returned by methods that
-              return a single attribute.
-            """,
-        "error":
-            """
-            :any:`IbisError`
-              If the method failed, details of the error.
-            """,
-        "people":
-            """
-            list of :any:`IbisPerson`
-              The list of people returned by methods that may return multiple
-              people. This may be empty, or contain one or more people.
-            """,
-        "institutions":
-            """
-            list of :any:`IbisInstitution`
-              The list of institutions returned by methods that may return
-              multiple institutions. This may be empty, or contain one or more
-              institutions.
-            """,
-        "groups":
-            """
-            list of :any:`IbisGroup`
-              The list of groups returned by methods that may return multiple
-              groups. This may be empty, or contain one or more groups.
-            """,
-        "attributes":
-            """
-            list of :any:`IbisAttribute`
-              The list of attributes returned by methods that return lists of
-              person/institution attributes.
-            """,
-        "attributeSchemes":
-            """
-            list of :any:`IbisAttributeScheme`
-              The list of attribute schemes returned by methods that return
-              lists of person/institution attribute schemes.
-            """,
-        "entities":
-            """
-            :any:`IbisResult.Entities`
-              In the flattened XML/JSON representation, all the unique
-              entities returned by the method.
-
-              .. note::
-                This will be :any:`None` unless the "flatten" parameter is
-                :any:`True`.
-            """
-    }
-
-    xml_attrs = set(["version"])
-
-    xml_elems = set(["value", "person", "institution", "group",
-                     "identifier", "attribute", "error", "entities"])
-
-    xml_arrays = set(["people", "institutions", "groups",
-                      "attributes", "attributeSchemes"])
-
-    class Entities(IbisDto):
-        """
-        Nested class to hold the full details of all the entities returned
-        in a result. This is used only in the flattened result representation,
-        where each of these entities will have a unique textual ID, and be
-        referred to from the top-level objects returned (and by each other).
-
-        In the hierarchical representation, this is not used, since all
-        entities returned will be at the top-level, or directly contained in
-        those top-level entities.
-
-        .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-        """
-        __slots__ = {
-            "people":
-                """
-                list of :any:`IbisPerson`
-                  A list of all the unique people returned by the method. This
-                  may include additional people returned as a result of the
-                  `fetch` parameter, so this list may contain more entries
-                  than the corresponding field on the enclosing class.
-                """,
-            "institutions":
-                """
-                list of :any:`IbisInstitution`
-                  A list of all the unique institutions returned by the
-                  method. This may include additional institutions returned as
-                  a result of the `fetch` parameter, so this list may contain
-                  more entries than the corresponding field on the enclosing
-                  class.
-                """,
-            "groups":
-                """
-                list of :any:`IbisGroup`
-                  A list of all the unique groups returned by the method. This
-                  may include additional groups returned as a result of the
-                  `fetch` parameter, so this list may contain more entries
-                  than the corresponding field on the enclosing class.
-                """
-        }
-
-        xml_arrays = set(["people", "institutions", "groups"])
-
-    class EntityMap:
-        """
-        Nested class to assist during the unflattening process, maintaining
-        efficient maps from IDs to entities (people, institutions and groups).
-
-        .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-        """
-        def __init__(self, result):
-            """
-            Construct an entity map from a flattened IbisResult.
-            """
-            self.people_by_id = {}
-            self.insts_by_id = {}
-            self.groups_by_id = {}
-
-            if result.entities.people:
-                for person in result.entities.people:
-                    self.people_by_id[person.id] = person
-            if result.entities.institutions:
-                for inst in result.entities.institutions:
-                    self.insts_by_id[inst.id] = inst
-            if result.entities.groups:
-                for group in result.entities.groups:
-                    self.groups_by_id[group.id] = group
-
-        def get_person(self, id):
-            """
-            Get a person from the entity map, given their ID.
-            """
-            return self.people_by_id.get(id)
-
-        def get_institution(self, id):
-            """
-            Get an institution from the entity map, given its ID.
-            """
-            return self.insts_by_id.get(id)
-
-        def get_group(self, id):
-            """
-            Get a group from the entity map, given its ID.
-            """
-            return self.groups_by_id.get(id)
-
-    def unflatten(self):
-        """
-        Unflatten this IbisResult object, resolving any internal ID refs
-        to build a fully fledged object tree.
-
-        This is necessary if the IbisResult was constructed from XML/JSON in
-        its flattened representation (with the "flatten" parameter set to
-        :any:`True`).
-
-        On entry, the IbisResult object may have people, institutions or
-        groups in it with "ref" fields referring to objects held in the
-        "entities" lists. After unflattening, all such references will have
-        been replaced by actual object references, giving an object tree that
-        can be traversed normally.
-
-        Returns this IbisResult object, with its internals unflattened.
-        """
-        if self.entities:
-            em = IbisResult.EntityMap(self)
-
-            if self.person:
-                self.person = self.person.unflatten(em)
-            if self.institution:
-                self.institution = self.institution.unflatten(em)
-            if self.group:
-                self.group = self.group.unflatten(em)
-
-            unflatten_people(em, self.people)
-            unflatten_insts(em, self.institutions)
-            unflatten_groups(em, self.groups)
-
-        return self
-
-# --------------------------------------------------------------------------
-# IbisResultParser: unmarshaller for IbisResult objects
-# --------------------------------------------------------------------------
-class IbisResultParser:
-    """
-    Class to parse the XML from the server and produce an IbisResult.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self):
-        self.result = None
-        self.node_stack = []
-        self.parser = expat.ParserCreate()
-        self.parser.StartElementHandler = self.start_element
-        self.parser.EndElementHandler = self.end_element
-        self.parser.CharacterDataHandler = self.char_data
-
-    def start_element(self, tagname, attrs):
-        element = None
-        if self.node_stack:
-            if tagname == "person":
-                element = IbisPerson(attrs)
-            elif tagname == "institution":
-                element = IbisInstitution(attrs)
-            elif tagname == "membersOfInst":
-                element = IbisInstitution(attrs)
-            elif tagname == "group":
-                element = IbisGroup(attrs)
-            elif tagname == "identifier":
-                element = IbisIdentifier(attrs)
-            elif tagname == "attribute":
-                element = IbisAttribute(attrs)
-            elif tagname == "error":
-                element = IbisError(attrs)
-            elif tagname == "attributeScheme":
-                element = IbisAttributeScheme(attrs)
-            elif tagname == "contactRow":
-                element = IbisContactRow(attrs)
-            elif tagname == "phoneNumber":
-                element = IbisContactPhoneNumber(attrs)
-            elif tagname == "webPage":
-                element = IbisContactWebPage(attrs)
-            elif tagname == "entities":
-                element = IbisResult.Entities(attrs)
-            else:
-                parent = self.node_stack[-1]
-                if (not isinstance(parent, list)) and\
-                   (not isinstance(parent, dict)):
-                    element = parent.start_child_element(tagname)
-            if element == None:
-                element = {"tagname": tagname}
-        elif tagname != "result":
-            raise Exception("Invalid root element: '%s'" % tagname)
-        else:
-            element = IbisResult(attrs)
-            self.result = element
-        self.node_stack.append(element)
-
-    def end_element(self, tagname):
-        if self.node_stack:
-            element = self.node_stack[-1]
-            self.node_stack.pop()
-            if self.node_stack:
-                parent = self.node_stack[-1]
-                if isinstance(parent, list):
-                    if isinstance(element, dict):
-                        parent.append(element.get("data"))
-                    else:
-                        parent.append(element)
-                elif not isinstance(parent, dict):
-                    if isinstance(element, dict):
-                        data = element.get("data")
-                    else:
-                        data = element
-                    parent.end_child_element(tagname, data)
-        else:
-            raise Exception("Unexpected closing tag: '%s'" % tagname)
-
-    def char_data(self, data):
-        if self.node_stack:
-            element = self.node_stack[-1]
-            if isinstance(element, IbisIdentifier):
-                if element.value != None: element.value += data
-                else: element.value = data
-            elif isinstance(element, dict):
-                if PY3:
-                    if "data" in element: element["data"] += data
-                    else: element["data"] = data
-                else:
-                    if element.has_key("data"): element["data"] += data
-                    else: element["data"] = data
-
-    def parse_xml(self, data):
-        """
-        Parse XML data from the specified string and return an IbisResult.
-
-        **Parameters**
-          `data` : str
-            [required] The XML string returned from the server.
-
-        **Returns**
-          :any:`IbisResult`
-            The parsed results. This may contain lists or trees of objects
-            representing people, institutions and groups returned from the
-            server.
-        """
-        self.parser.Parse(data)
-        return self.result.unflatten()
-
-    def parse_xml_file(self, file):
-        """
-        Parse XML data from the specified file and return an IbisResult.
-
-        **Parameters**
-          `file` : file
-            [required] A file object containing XML returned from the server.
-
-        **Returns**
-          :any:`IbisResult`
-            The parsed results. This may contain lists or trees of objects
-            representing people, institutions and groups returned from the
-            server.
-        """
-        self.parser.ParseFile(file)
-        return self.result.unflatten()
diff --git a/ucamlookup/ibisclient/methods.py b/ucamlookup/ibisclient/methods.py
deleted file mode 100644
index 724f8ad964627fb043fa46b1a284bbfbc196709f..0000000000000000000000000000000000000000
--- a/ucamlookup/ibisclient/methods.py
+++ /dev/null
@@ -1,2201 +0,0 @@
-# === AUTO-GENERATED - DO NOT EDIT ===
-
-# --------------------------------------------------------------------------
-# Copyright (c) 2012, University of Cambridge Computing Service
-#
-# This file is part of the Lookup/Ibis client library.
-#
-# This library is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This library is distributed in the hope that it will be useful, but
-# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
-# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
-# License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this library.  If not, see <http://www.gnu.org/licenses/>.
-# --------------------------------------------------------------------------
-
-"""
-Web service API methods. This module is fully auto-generated, and contains
-the Python equivalent of the `XxxMethods` Java classes for executing all API
-methods.
-"""
-
-from .connection import IbisException
-
-class IbisMethods:
-    """
-    Common methods for searching for objects in the Lookup/Ibis database.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, conn):
-        self.conn = conn
-
-    def getVersion(self):
-        """
-        Get the current API version number.
-
-        ``[ HTTP: GET /api/v1/version ]``
-
-        **Returns**
-          str
-            The API version number string.
-        """
-        path = "api/v1/version"
-        path_params = {}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.value
-
-class GroupMethods:
-    """
-    Methods for querying and manipulating groups.
-
-    **The fetch parameter for groups**
-
-    All methods that return groups also accept an optional `fetch`
-    parameter that may be used to request additional information about the
-    groups returned. For more details about the general rules that apply to
-    the `fetch` parameter, refer to the :any:`PersonMethods`
-    documentation.
-
-    For groups the `fetch` parameter may be used to fetch references
-    to people, institutions or other groups. In each case, only non-cancelled
-    people, institutions and groups will be included when fetching references.
-    The following references are supported:
-
-    * ``"all_members"`` - fetches all the people who are members of the
-      group, including members of groups included by the group, and groups
-      included by those groups, and so on.
-
-    * ``"direct_members"`` - fetches all the poeple who are direct
-      members of the group, not taking into account any included groups.
-
-    * ``"members_of_inst"`` - if the group is a membership group for an
-      institution, this fetches that institution.
-
-    * ``"owning_insts"`` - fetches all the institutions to which the
-      group belongs.
-
-    * ``"manages_insts"`` - fetches all the institutions that the group
-      manages. Typically this only applies to "Editor" groups.
-
-    * ``"manages_groups"`` - fetches all the groups that this group
-      manages. Note that some groups are self-managed, so this may be a
-      self-reference.
-
-    * ``"managed_by_groups"`` - fetches all the groups that manage this
-      group.
-
-    * ``"reads_groups"`` - fetches all the groups that this group has
-      privileged access to. This means that members of this group can see the
-      members of the referenced groups regardless of the membership visibility
-      settings.
-
-    * ``"read_by_groups"`` - fetches all the groups that have privileged
-      access to this group.
-
-    * ``"includes_groups"`` - fetches all the groups included by this
-      group.
-
-    * ``"included_by_groups"`` - fetches all the groups that include
-      this group.
-
-    As with person `fetch` parameters, the references may be used
-    in a chain by using the "dot" notation to fetch additional information
-    about referenced people, institutions or groups. For example
-    ``"all_members.email"`` will fetch the email addresses of all members
-    of the group. For more information about what can be fetched from
-    referenced people and institutions, refer to the documentation for
-    :any:`PersonMethods` and :any:`InstitutionMethods`.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, conn):
-        self.conn = conn
-
-    def allGroups(self,
-                  includeCancelled,
-                  fetch=None):
-        """
-        Return a list of all groups.
-
-        By default, only a few basic details about each group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/group/all-groups ]``
-
-        **Parameters**
-          `includeCancelled` : bool
-            [optional] Whether or not to include cancelled
-            groups. By default, only live groups are returned.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisGroup`
-            The requested groups (in groupid order).
-        """
-        path = "api/v1/group/all-groups"
-        path_params = {}
-        query_params = {"includeCancelled": includeCancelled,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.groups
-
-    def listGroups(self,
-                   groupids,
-                   fetch=None):
-        """
-        Get the groups with the specified IDs or names.
-
-        By default, only a few basic details about each group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        The results are sorted by groupid.
-
-        .. note::
-          The URL path length is limited to around 8000 characters,
-          which limits the number of groups that this method can fetch. Group
-          IDs are currently 6 characters long, and must be comma separated and
-          URL encoded, which limits this method to around 800 groups by ID,
-          but probably fewer by name, depending on the group name lengths.
-
-        .. note::
-          The groups returned may include cancelled groups. It is the
-          caller's repsonsibility to check their cancelled flags.
-
-        ``[ HTTP: GET /api/v1/group/list?groupids=... ]``
-
-        **Parameters**
-          `groupids` : str
-            [required] A comma-separated list of group IDs or
-            group names (may be a mix of both).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisGroup`
-            The requested groups (in groupid order).
-        """
-        path = "api/v1/group/list"
-        path_params = {}
-        query_params = {"groupids": groupids,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.groups
-
-    def search(self,
-               query,
-               approxMatches=None,
-               includeCancelled=None,
-               offset=None,
-               limit=None,
-               orderBy=None,
-               fetch=None):
-        """
-        Search for groups using a free text query string. This is the same
-        search function that is used in the Lookup web application.
-
-        By default, only a few basic details about each group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/group/search?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled groups to
-            be included. Defaults to :any:`False`.
-
-          `offset` : int
-            [optional] The number of results to skip at the start
-            of the search. Defaults to 0.
-
-          `limit` : int
-            [optional] The maximum number of results to return.
-            Defaults to 100.
-
-          `orderBy` : str
-            [optional] The order in which to list the results.
-            This may be ``"groupid"``, ``"name"`` (the default) or
-            ``"title"``.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisGroup`
-            The matching groups.
-        """
-        path = "api/v1/group/search"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled,
-                        "offset": offset,
-                        "limit": limit,
-                        "orderBy": orderBy,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.groups
-
-    def searchCount(self,
-                    query,
-                    approxMatches=None,
-                    includeCancelled=None):
-        """
-        Count the number of groups that would be returned by a search using
-        a free text query string.
-
-        ``[ HTTP: GET /api/v1/group/search-count?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled groups to
-            be included. Defaults to :any:`False`.
-
-        **Returns**
-          int
-            The number of matching groups.
-        """
-        path = "api/v1/group/search-count"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return int(result.value)
-
-    def getGroup(self,
-                 groupid,
-                 fetch=None):
-        """
-        Get the group with the specified ID or name.
-
-        By default, only a few basic details about the group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of the group.
-
-        .. note::
-          The group returned may be a cancelled group. It is the caller's
-          repsonsibility to check its cancelled flag.
-
-        ``[ HTTP: GET /api/v1/group/{groupid} ]``
-
-        **Parameters**
-          `groupid` : str
-            [required] The ID or name of the group to fetch. This
-            may be either the numeric ID or the short hyphenated group name (for
-            example ``"100656"`` or ``"cs-editors"``).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          :any:`IbisGroup`
-            The requested group or :any:`None` if it was not found.
-        """
-        path = "api/v1/group/%(groupid)s"
-        path_params = {"groupid": groupid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.group
-
-    def getCancelledMembers(self,
-                            groupid,
-                            fetch=None):
-        """
-        Get all the cancelled members of the specified group, including
-        cancelled members of groups included by the group, and groups included
-        by those groups, and so on.
-
-        By default, only a few basic details about each member are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each person.
-
-        .. note::
-          This method returns only cancelled people. It does not include
-          people who were removed from the group. Cancelled people are no longer
-          considered to be current staff, students or accredited visitors, and
-          are no longer regarded as belonging to any groups or institutions. The
-          list returned here reflects their group memberships just before they
-          were cancelled, and so is out-of-date data that should be used with
-          caution.
-
-        ``[ HTTP: GET /api/v1/group/{groupid}/cancelled-members ]``
-
-        **Parameters**
-          `groupid` : str
-            [required] The ID or name of the group.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for each person.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The group's cancelled members (in identifier order).
-        """
-        path = "api/v1/group/%(groupid)s/cancelled-members"
-        path_params = {"groupid": groupid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def getDirectMembers(self,
-                         groupid,
-                         fetch=None):
-        """
-        Get the direct members of the specified group, not including members
-        included via groups included by the group.
-
-        By default, only a few basic details about each member are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each person.
-
-        .. note::
-          This method will not include cancelled people.
-
-        ``[ HTTP: GET /api/v1/group/{groupid}/direct-members ]``
-
-        **Parameters**
-          `groupid` : str
-            [required] The ID or name of the group.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for each person.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The group's direct members (in identifier order).
-        """
-        path = "api/v1/group/%(groupid)s/direct-members"
-        path_params = {"groupid": groupid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def updateDirectMembers(self,
-                            groupid,
-                            addIds=None,
-                            removeIds=None,
-                            commitComment=None):
-        """
-        Update the list of people who are direct members of the group. This
-        will not affect people who are included in the group due to the
-        inclusion of other groups.
-
-        Any non-cancelled people in the list of identifiers specified by
-        `addIds` will be added to the group. This list should be a
-        comma-separated list of identifiers, each of which may be either a
-        CRSid or an identifier from another identifier scheme, prefixed with
-        that scheme's name and a slash. For example ``"mug99"`` or
-        ``"usn/123456789"``.
-
-        Any people in the list of identifiers specified by `removeIds`
-        will be removed from the group, except if they are also in the list
-        `addIds`. The special identifier ``"all-members"`` may be
-        used to remove all existing group members, replacing them with the
-        list specified by `newIds`.
-
-        **Examples:**
-
-        .. code-block:: python
-
-          updateDirectMembers("test-group",
-                              "mug99,crsid/yyy99,usn/123456789",
-                              "xxx99",
-                              "Remove xxx99 and add mug99, yyy99 and usn/123456789 to test-group");
-
-        .. code-block:: python
-
-          updateDirectMembers("test-group",
-                              "xxx99,yyy99",
-                              "all-members",
-                              "Set the membership of test-group to include only xxx99 and yyy99");
-
-        ``[ HTTP: PUT /api/v1/group/{groupid}/direct-members ]``
-
-        **Parameters**
-          `groupid` : str
-            [required] The ID or name of the group.
-
-          `addIds` : str
-            [optional] The identifiers of people to add to the group.
-
-          `removeIds` : str
-            [optional] The identifiers of people to remove from
-            the group.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab of the group and
-            all the affected people in the web application).
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The updated list of direct members of the group (in identifier
-            order).
-        """
-        path = "api/v1/group/%(groupid)s/direct-members"
-        path_params = {"groupid": groupid}
-        query_params = {}
-        form_params = {"addIds": addIds,
-                       "removeIds": removeIds,
-                       "commitComment": commitComment}
-        result = self.conn.invoke_method("PUT", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def getMembers(self,
-                   groupid,
-                   fetch=None):
-        """
-        Get all the members of the specified group, including members of
-        groups included by the group, and groups included by those groups,
-        and so on.
-
-        By default, only a few basic details about each member are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each person.
-
-        .. note::
-          This method will not include cancelled people.
-
-        ``[ HTTP: GET /api/v1/group/{groupid}/members ]``
-
-        **Parameters**
-          `groupid` : str
-            [required] The ID or name of the group.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for each person.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The group's members (in identifier order).
-        """
-        path = "api/v1/group/%(groupid)s/members"
-        path_params = {"groupid": groupid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-class InstitutionMethods:
-    """
-    Methods for querying and manipulating institutions.
-
-    **The fetch parameter for institutions**
-
-    All methods that return institutions also accept an optional
-    `fetch` parameter that may be used to request additional
-    information about the institutions returned. For more details about
-    the general rules that apply to the `fetch` parameter,
-    refer to the :any:`PersonMethods` documentation.
-
-    For institutions the `fetch` parameter may be used to fetch
-    any institution attribute by specifying the `schemeid` of an
-    institution attribute scheme. Examples include ``"address"``,
-    ``"jpegPhoto"``, ``"universityPhone"``, ``"instPhone"``,
-    ``"landlinePhone"``, ``"mobilePhone"``, ``"faxNumber"``,
-    ``"email"`` and ``"labeledURI"``. The full list (which may be
-    extended over time) may be obtained using :any:`InstitutionMethods.allAttributeSchemes()`.
-
-    In addition the following pseudo-attributes are supported:
-
-    * ``"phone_numbers"`` - fetches all phone numbers. This is
-      equivalent to
-      ``"universityPhone,instPhone,landlinePhone,mobilePhone"``.
-
-    * ``"all_attrs"`` - fetches all attributes from all institution
-      attribute schemes. This does not include references.
-
-    * ``"contact_rows"`` - fetches all institution contact rows. Any
-      chained fetches from contact rows are used to fetch attributes from any
-      people referred to by the contact rows.
-
-    The `fetch` parameter may also be used to fetch referenced
-    people, institutions or groups. This will only include references to
-    non-cancelled entities. The following references are supported:
-
-    * ``"all_members"`` - fetches all the people who are members of the
-      institution.
-
-    * ``"parent_insts"`` - fetches all the parent institutions. Note
-      that currently all institutions have only one parent, but this may change
-      in the future, and client applications should be prepared to handle
-      multiple parents.
-
-    * ``"child_insts"`` - fetches all the child institutions.
-
-    * ``"inst_groups"`` - fetches all the groups that belong to the
-      institution.
-
-    * ``"members_groups"`` - fetches all the groups that form the
-      institution's membership list.
-
-    * ``"managed_by_groups"`` - fetches all the groups that manage the
-      institution's data (commonly called "Editor" groups).
-
-    As with person `fetch` parameters, the references may be used
-    in a chain by using the "dot" notation to fetch additional information
-    about referenced people, institutions or groups. For example
-    ``"all_members.email"`` will fetch the email addresses of all members
-    of the institution. For more information about what can be fetched from
-    referenced people and groups, refer to the documentation for
-    :any:`PersonMethods` and :any:`GroupMethods`.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, conn):
-        self.conn = conn
-
-    def allAttributeSchemes(self):
-        """
-        Return a list of all the institution attribute schemes available.
-        The `schemeid` values of these schemes may be used in the
-        `fetch` parameter of other methods that return institutions.
-
-        ``[ HTTP: GET /api/v1/inst/all-attr-schemes ]``
-
-        **Returns**
-          list of :any:`IbisAttributeScheme`
-            All the available institution attribute schemes (in precedence
-            order).
-        """
-        path = "api/v1/inst/all-attr-schemes"
-        path_params = {}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attributeSchemes
-
-    def allInsts(self,
-                 includeCancelled,
-                 fetch=None):
-        """
-        Return a list of all institutions.
-
-        By default, only a few basic details about each institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/inst/all-insts ]``
-
-        **Parameters**
-          `includeCancelled` : bool
-            [optional] Whether or not to include cancelled
-            institutions. By default, only live institutions are returned.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisInstitution`
-            The requested institutions (in instid order).
-        """
-        path = "api/v1/inst/all-insts"
-        path_params = {}
-        query_params = {"includeCancelled": includeCancelled,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institutions
-
-    def listInsts(self,
-                  instids,
-                  fetch=None):
-        """
-        Get the institutions with the specified IDs.
-
-        By default, only a few basic details about each institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references.
-
-        The results are sorted by ID.
-
-        .. note::
-          The URL path length is limited to around 8000 characters, and
-          an instid is up to 8 characters long. Allowing for comma separators
-          and URL encoding, this limits the number of institutions that this
-          method may fetch to around 700.
-
-        .. note::
-          The institutions returned may include cancelled institutions.
-          It is the caller's repsonsibility to check their cancelled flags.
-
-        ``[ HTTP: GET /api/v1/inst/list?instids=... ]``
-
-        **Parameters**
-          `instids` : str
-            [required] A comma-separated list of instids.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisInstitution`
-            The requested institutions (in instid order).
-        """
-        path = "api/v1/inst/list"
-        path_params = {}
-        query_params = {"instids": instids,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institutions
-
-    def search(self,
-               query,
-               approxMatches=None,
-               includeCancelled=None,
-               attributes=None,
-               offset=None,
-               limit=None,
-               orderBy=None,
-               fetch=None):
-        """
-        Search for institutions using a free text query string. This is the
-        same search function that is used in the Lookup web application.
-
-        By default, only a few basic details about each institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/inst/search?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled institutions
-            to be included. Defaults to :any:`False`.
-
-          `attributes` : str
-            [optional] A comma-separated list of attributes to
-            consider when searching. If this is :any:`None` (the default) then
-            all attribute schemes marked as searchable will be included.
-
-          `offset` : int
-            [optional] The number of results to skip at the start
-            of the search. Defaults to 0.
-
-          `limit` : int
-            [optional] The maximum number of results to return.
-            Defaults to 100.
-
-          `orderBy` : str
-            [optional] The order in which to list the results.
-            This may be either ``"instid"`` or ``"name"`` (the default).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisInstitution`
-            The matching institutions.
-        """
-        path = "api/v1/inst/search"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled,
-                        "attributes": attributes,
-                        "offset": offset,
-                        "limit": limit,
-                        "orderBy": orderBy,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institutions
-
-    def searchCount(self,
-                    query,
-                    approxMatches=None,
-                    includeCancelled=None,
-                    attributes=None):
-        """
-        Count the number of institutions that would be returned by a search
-        using a free text query string.
-
-        ``[ HTTP: GET /api/v1/inst/search-count?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled institutions
-            to be included. Defaults to :any:`False`.
-
-          `attributes` : str
-            [optional] A comma-separated list of attributes to
-            consider when searching. If this is :any:`None` (the default) then
-            all attribute schemes marked as searchable will be included.
-
-        **Returns**
-          int
-            The number of matching institutions.
-        """
-        path = "api/v1/inst/search-count"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled,
-                        "attributes": attributes}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return int(result.value)
-
-    def getInst(self,
-                instid,
-                fetch=None):
-        """
-        Get the institution with the specified ID.
-
-        By default, only a few basic details about the institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references of the institution.
-
-        .. note::
-          The institution returned may be a cancelled institution. It is
-          the caller's repsonsibility to check its cancelled flag.
-
-        ``[ HTTP: GET /api/v1/inst/{instid} ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution to fetch.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          :any:`IbisInstitution`
-            The requested institution or :any:`None` if it was not found.
-        """
-        path = "api/v1/inst/%(instid)s"
-        path_params = {"instid": instid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institution
-
-    def addAttribute(self,
-                     instid,
-                     attr,
-                     position=None,
-                     allowDuplicates=None,
-                     commitComment=None):
-        """
-        Add an attribute to an institution. By default, this will not add the
-        attribute again if it already exists.
-
-        When adding an attribute, the new attribute's scheme must be set.
-        In addition, either its value or its binaryData field should be set.
-        All the remaining fields of the attribute are optional.
-
-        ``[ HTTP: POST /api/v1/inst/{instid}/add-attribute ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `attr` : :any:`IbisAttribute`
-            [required] The new attribute to add.
-
-          `position` : int
-            [optional] The position of the new attribute in the
-            list of attributes of the same attribute scheme (1, 2, 3,...). A value
-            of 0 (the default) will cause the new attribute to be added to the end
-            of the list of existing attributes for the scheme.
-
-          `allowDuplicates` : bool
-            [optional] If :any:`True`, the new attribute
-            will always be added, even if another identical attribute already
-            exists. If :any:`False` (the default), the new attribute will only be
-            added if it doesn't already exist.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          :any:`IbisAttribute`
-            The newly created or existing attribute.
-        """
-        path = "api/v1/inst/%(instid)s/add-attribute"
-        path_params = {"instid": instid}
-        query_params = {}
-        form_params = {"attr": attr,
-                       "position": position,
-                       "allowDuplicates": allowDuplicates,
-                       "commitComment": commitComment}
-        result = self.conn.invoke_method("POST", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute
-
-    def getCancelledMembers(self,
-                            instid,
-                            fetch=None):
-        """
-        Get all the cancelled members of the specified institution.
-
-        By default, only a few basic details about each member are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each person.
-
-        .. note::
-          This method returns only cancelled people. It does not include
-          people who were removed from the institution. Cancelled people are no
-          longer considered to be current staff, students or accredited visitors,
-          and are no longer regarded as belonging to any groups or institutions.
-          The list returned here reflects their institutional memberships just
-          before they were cancelled, and so is out-of-date data that should be
-          used with caution.
-
-        ``[ HTTP: GET /api/v1/inst/{instid}/cancelled-members ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for each person.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The institution's cancelled members (in identifier order).
-        """
-        path = "api/v1/inst/%(instid)s/cancelled-members"
-        path_params = {"instid": instid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def getContactRows(self,
-                       instid,
-                       fetch=None):
-        """
-        Get all the contact rows of the specified institution.
-
-        Any addresses, email addresses, phone numbers and web pages
-        associated with the contact rows are automatically returned, as
-        well as any people referred to by the contact rows.
-
-        If any of the contact rows refer to people, then only a few basic
-        details about each person are returned, but the optional
-        `fetch` parameter may be used to fetch additional
-        attributes or references of each person.
-
-        .. note::
-          This method will not include cancelled people.
-
-        ``[ HTTP: GET /api/v1/inst/{instid}/contact-rows ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for any people referred to by any
-            of the contact rows.
-
-        **Returns**
-          list of :any:`IbisContactRow`
-            The institution's contact rows.
-        """
-        path = "api/v1/inst/%(instid)s/contact-rows"
-        path_params = {"instid": instid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institution.contactRows
-
-    def getAttributes(self,
-                      instid,
-                      attrs):
-        """
-        Get one or more (possibly multi-valued) attributes of an institution.
-        The returned attributes are sorted by attribute scheme precedence and
-        then attribute precedence.
-
-        ``[ HTTP: GET /api/v1/inst/{instid}/get-attributes?attrs=... ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `attrs` : str
-            [required] The attribute scheme(s) to fetch. This may
-            include any number of the attributes or pseudo-attributes, but it
-            may not include references or attribute chains (see the documentation
-            for the `fetch` parameter in this class).
-
-        **Returns**
-          list of :any:`IbisAttribute`
-            The requested attributes.
-        """
-        path = "api/v1/inst/%(instid)s/get-attributes"
-        path_params = {"instid": instid}
-        query_params = {"attrs": attrs}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attributes
-
-    def getMembers(self,
-                   instid,
-                   fetch=None):
-        """
-        Get all the members of the specified institution.
-
-        By default, only a few basic details about each member are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each person.
-
-        .. note::
-          This method will not include cancelled people.
-
-        ``[ HTTP: GET /api/v1/inst/{instid}/members ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch for each person.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The institution's members (in identifier order).
-        """
-        path = "api/v1/inst/%(instid)s/members"
-        path_params = {"instid": instid}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def deleteAttribute(self,
-                        instid,
-                        attrid,
-                        commitComment=None):
-        """
-        Delete an attribute of an institution. It is not an error if the
-        attribute does not exist.
-
-        Note that in this method, the `commitComment` is passed
-        as a query parameter, rather than as a form parameter, for greater
-        client compatibility.
-
-        ``[ HTTP: DELETE /api/v1/inst/{instid}/{attrid} ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `attrid` : long
-            [required] The ID of the attribute to delete.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          bool
-            :any:`True` if the attribute was deleted by this method, or
-            :any:`False` if it did not exist.
-        """
-        path = "api/v1/inst/%(instid)s/%(attrid)s"
-        path_params = {"instid": instid,
-                       "attrid": attrid}
-        query_params = {"commitComment": commitComment}
-        form_params = {}
-        result = self.conn.invoke_method("DELETE", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.value and result.value.lower() == "true"
-
-    def getAttribute(self,
-                     instid,
-                     attrid):
-        """
-        Get a specific attribute of an institution.
-
-        ``[ HTTP: GET /api/v1/inst/{instid}/{attrid} ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `attrid` : long
-            [required] The ID of the attribute to fetch.
-
-        **Returns**
-          :any:`IbisAttribute`
-            The requested attribute.
-        """
-        path = "api/v1/inst/%(instid)s/%(attrid)s"
-        path_params = {"instid": instid,
-                       "attrid": attrid}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute
-
-    def updateAttribute(self,
-                        instid,
-                        attrid,
-                        attr,
-                        commitComment=None):
-        """
-        Update an attribute of an institution.
-
-        The attribute's value, binaryData, comment and effective date fields
-        will all be updated using the data supplied. All other fields will be
-        left unchanged.
-
-        To avoid inadvertently changing fields of the attribute, it is
-        recommended that :any:`getAttribute() <InstitutionMethods.getAttribute>` be used to
-        retrieve the current value of the attribute, before calling this
-        method with the required changes.
-
-        ``[ HTTP: PUT /api/v1/inst/{instid}/{attrid} ]``
-
-        **Parameters**
-          `instid` : str
-            [required] The ID of the institution.
-
-          `attrid` : long
-            [required] The ID of the attribute to update.
-
-          `attr` : :any:`IbisAttribute`
-            [required] The new attribute values to apply.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          :any:`IbisAttribute`
-            The updated attribute.
-        """
-        path = "api/v1/inst/%(instid)s/%(attrid)s"
-        path_params = {"instid": instid,
-                       "attrid": attrid}
-        query_params = {}
-        form_params = {"attr": attr,
-                       "commitComment": commitComment}
-        result = self.conn.invoke_method("PUT", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute
-
-class PersonMethods:
-    """
-    Methods for querying and manipulating people.
-
-    **Notes on the fetch parameter**
-
-    All methods that return people, institutions or groups also accept an
-    optional `fetch` parameter that may be used to request
-    additional information about the entities returned. Without this
-    parameter, only a few basic details about each person, institution or
-    group are returned. The `fetch` parameter is quite flexible,
-    and may be used in a number of different ways:
-
-    * **Attribute fetching**. Attributes may be fetched by specifying the
-      `schemeid` of an attribute scheme. For example to fetch a
-      person's email addresses, use the value ``"email"``. For people common
-      attribute schemes include ``"jpegPhoto"``, ``"misAffiliation"``,
-      ``"title"``, ``"universityPhone"``, ``"mobexPhone"``,
-      ``"landlinePhone"``, ``"mobilePhone"``, ``"pager"``,
-      ``"labeledURI"`` and ``"address"``. The full list of person
-      attribute schemes may be obtained using :any:`PersonMethods.allAttributeSchemes()`.
-
-    * **Pseudo-attributes**. Certain special pseudo-attributes are defined
-      for convenience. For people, the following pseudo-attributes are supported:
-
-      * ``"phone_numbers"`` - fetches all phone numbers. This is
-        equivalent to
-        ``"universityPhone,instPhone,mobexPhone,landlinePhone,mobilePhone,pager"``.
-
-      * ``"all_identifiers"`` - fetches all identifiers. Currently people
-        only have CRSid identifiers, but in the future additional identifiers such
-        as USN or staffNumber may be added.
-
-      * ``"all_attrs"`` - fetches all attributes from all person attribute
-        schemes. This does not include identifiers or references.
-
-    * **Reference fetching**. For people, the following references are
-      supported (and will fetch only non-cancelled institutions and groups):
-
-      * ``"all_insts"`` - fetches all the institutions to which the person
-        belongs (sorted in name order).
-
-      * ``"all_groups"`` - fetches all the groups that the person is a
-        member of, including indirect group memberships, via groups that include
-        other groups.
-
-      * ``"direct_groups"`` - fetches all the groups that the person is
-        directly a member of. This does not include indirect group memberships -
-        i.e., groups that include these groups.
-
-    * **Chained reference fetching**. To fetch properties of referenced
-      objects, the "dot" notation may be used. For example, to fetch the email
-      addresses of all the institutions to which a person belongs, use
-      ``"all_insts.email"``. Chains may include a number of reference
-      following steps, for example
-      ``"all_insts.managed_by_groups.all_members.email"`` will fetch all the
-      institutions to which the person belongs, all the groups that manage those
-      institutions, all the visible members of those groups and all the email
-      addresses of those managing group members. For more information about what
-      can be fetched from referenced institutions and groups, refer to the
-      documentation for :any:`InstitutionMethods` and :any:`GroupMethods`.
-
-    Multiple values of the `fetch` parameter should be separated
-    by commas.
-
-    **Fetch parameter examples**
-
-    ``fetch = "email"``
-    This fetches all the person's email addresses.
-
-    ``fetch = "title,address"``
-    This fetches all the person's titles (roles) and addresses.
-
-    ``fetch = "all_attrs"``
-    This fetches all the person's attributes.
-
-    ``fetch = "all_groups,all_insts"``
-    This fetches all the groups and institutions to which the person belongs.
-
-    ``fetch = "all_insts.parent_insts"``
-    This fetches all the person's institutions, and their parent institutions.
-
-    ``fetch = "all_insts.email,all_insts.all_members.email"``
-    This fetches all the person's institutions and their email addresses, and
-    all the members of those institutions, and the email addresses of all
-    those members.
-
-    .. codeauthor:: Dean Rasheed (dev-group@ucs.cam.ac.uk)
-    """
-    def __init__(self, conn):
-        self.conn = conn
-
-    def allAttributeSchemes(self):
-        """
-        Return a list of all the person attribute schemes available. The
-        `schemeid` values of these schemes may be used in the
-        `fetch` parameter of other methods that return people.
-
-        .. note::
-          Some of these attribute schemes are not currently used (no
-          people have attribute values in the scheme). These schemes are
-          reserved for possible future use.
-
-        ``[ HTTP: GET /api/v1/person/all-attr-schemes ]``
-
-        **Returns**
-          list of :any:`IbisAttributeScheme`
-            All the available person attribute schemes (in precedence
-            order).
-        """
-        path = "api/v1/person/all-attr-schemes"
-        path_params = {}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attributeSchemes
-
-    def allPeople(self,
-                  includeCancelled,
-                  identifier=None,
-                  limit=None,
-                  fetch=None):
-        """
-        Return a list of all people (in batches).
-
-        The results are sorted by identifier, starting with the first person
-        after the person with the specified identifier. Thus, to iterate over
-        all people, pass a :any:`None` identifier to get the first batch of
-        people, then pass the last identifier from the previous batch to get
-        the next batch, and repeat until no more people are returned.
-
-        By default, only a few basic details about each person are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/person/all-people ]``
-
-        **Parameters**
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled people to
-            be included (people who are no longer members of the University).
-            Defaults to :any:`False`.
-
-          `identifier` : str
-            [optional] The identifier (CRSid) of the person to
-            start after, or :any:`None` to start from the first person.
-
-          `limit` : int
-            [optional] The maximum number of people to return.
-            Defaults to 100.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The requested people (in identifier order).
-        """
-        path = "api/v1/person/all-people"
-        path_params = {}
-        query_params = {"includeCancelled": includeCancelled,
-                        "identifier": identifier,
-                        "limit": limit,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def listPeople(self,
-                   crsids,
-                   fetch=None):
-        """
-        Get the people with the specified identifiers (typically CRSids).
-
-        Each identifier may be either a CRSid, or an identifier from another
-        identifier scheme, prefixed with that scheme's name and a slash. For
-        example ``"mug99"`` or ``"usn/123456789"``.
-
-        By default, only a few basic details about each person are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        The results are sorted by identifier scheme and value.
-
-        .. note::
-          The number of people that may be fetched in a single call is
-          limited by the URL path length limit (around 8000 characters). A
-          CRSid is up to 7 characters long, and other identifiers are typically
-          longer, since they must also include the identifier scheme. Thus the
-          number of people that this method may fetch is typically limited to a
-          few hundred.
-
-        .. note::
-          The people returned may include cancelled people. It is the
-          caller's repsonsibility to check their cancelled flags.
-
-        ``[ HTTP: GET /api/v1/person/list?crsids=... ]``
-
-        **Parameters**
-          `crsids` : str
-            [required] A comma-separated list of identifiers.
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The requested people (in identifier order).
-        """
-        path = "api/v1/person/list"
-        path_params = {}
-        query_params = {"crsids": crsids,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def search(self,
-               query,
-               approxMatches=None,
-               includeCancelled=None,
-               misStatus=None,
-               attributes=None,
-               offset=None,
-               limit=None,
-               orderBy=None,
-               fetch=None):
-        """
-        Search for people using a free text query string. This is the same
-        search function that is used in the Lookup web application.
-
-        By default, only a few basic details about each person are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references.
-
-        ``[ HTTP: GET /api/v1/person/search?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled people to
-            be included (people who are no longer members of the University).
-            Defaults to :any:`False`.
-
-          `misStatus` : str
-            [optional] The type of people to search for. This may
-            be
-
-            * ``"staff"`` - only include people whose MIS status is
-              ``""`` (empty string), ``"staff"``, or
-              ``"staff,student"``.
-
-            * ``"student"`` - only include people whose MIS status is set to
-              ``"student"`` or ``"staff,student"``.
-
-            Otherwise all matching people will be included (the default). Note
-            that the ``"staff"`` and ``"student"`` options are not
-            mutually exclusive.
-
-          `attributes` : str
-            [optional] A comma-separated list of attributes to
-            consider when searching. If this is :any:`None` (the default) then
-            all attribute schemes marked as searchable will be included.
-
-          `offset` : int
-            [optional] The number of results to skip at the start
-            of the search. Defaults to 0.
-
-          `limit` : int
-            [optional] The maximum number of results to return.
-            Defaults to 100.
-
-          `orderBy` : str
-            [optional] The order in which to list the results.
-            This may be either ``"identifier"`` or ``"surname"`` (the
-            default).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisPerson`
-            The matching people.
-        """
-        path = "api/v1/person/search"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled,
-                        "misStatus": misStatus,
-                        "attributes": attributes,
-                        "offset": offset,
-                        "limit": limit,
-                        "orderBy": orderBy,
-                        "fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.people
-
-    def searchCount(self,
-                    query,
-                    approxMatches=None,
-                    includeCancelled=None,
-                    misStatus=None,
-                    attributes=None):
-        """
-        Count the number of people that would be returned by a search using
-        a free text query string.
-
-        ``[ HTTP: GET /api/v1/person/search-count?query=... ]``
-
-        **Parameters**
-          `query` : str
-            [required] The search string.
-
-          `approxMatches` : bool
-            [optional] Flag to enable more approximate
-            matching in the search, causing more results to be returned. Defaults
-            to :any:`False`.
-
-          `includeCancelled` : bool
-            [optional] Flag to allow cancelled people to
-            be included (people who are no longer members of the University).
-            Defaults to :any:`False`.
-
-          `misStatus` : str
-            [optional] The type of people to search for. This may
-            be
-
-            * ``"staff"`` - only include people whose MIS status is
-              ``""`` (empty string), ``"staff"``, or
-              ``"staff,student"``.
-
-            * ``"student"`` - only include people whose MIS status is set to
-              ``"student"`` or ``"staff,student"``.
-
-            Otherwise all matching people will be included (the default). Note
-            that the ``"staff"`` and ``"student"`` options are not
-            mutually exclusive.
-
-          `attributes` : str
-            [optional] A comma-separated list of attributes to
-            consider when searching. If this is :any:`None` (the default) then
-            all attribute schemes marked as searchable will be included.
-
-        **Returns**
-          int
-            The number of matching people.
-        """
-        path = "api/v1/person/search-count"
-        path_params = {}
-        query_params = {"query": query,
-                        "approxMatches": approxMatches,
-                        "includeCancelled": includeCancelled,
-                        "misStatus": misStatus,
-                        "attributes": attributes}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return int(result.value)
-
-    def getPerson(self,
-                  scheme,
-                  identifier,
-                  fetch=None):
-        """
-        Get the person with the specified identifier.
-
-        By default, only a few basic details about the person are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of the person.
-
-        .. note::
-          The person returned may be a cancelled person. It is the
-          caller's repsonsibility to check its cancelled flag.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person to fetch
-            (typically their CRSid).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          :any:`IbisPerson`
-            The requested person or :any:`None` if they were not found.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.person
-
-    def addAttribute(self,
-                     scheme,
-                     identifier,
-                     attr,
-                     position=None,
-                     allowDuplicates=None,
-                     commitComment=None):
-        """
-        Add an attribute to a person. By default, this will not add the
-        attribute again if it already exists.
-
-        When adding an attribute, the new attribute's scheme must be set.
-        In addition, either its value or its binaryData field should be set.
-        All the remaining fields of the attribute are optional.
-
-        ``[ HTTP: POST /api/v1/person/{scheme}/{identifier}/add-attribute ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person to udpate
-            (typically their CRSid).
-
-          `attr` : :any:`IbisAttribute`
-            [required] The new attribute to add.
-
-          `position` : int
-            [optional] The position of the new attribute in the
-            list of attributes of the same attribute scheme (1, 2, 3,...). A value
-            of 0 (the default) will cause the new attribute to be added to the end
-            of the list of existing attributes for the scheme.
-
-          `allowDuplicates` : bool
-            [optional] If :any:`True`, the new attribute
-            will always be added, even if another identical attribute already
-            exists. If :any:`False` (the default), the new attribute will only be
-            added if it doesn't already exist.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          :any:`IbisAttribute`
-            The newly created or existing attribute.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/add-attribute"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {}
-        form_params = {"attr": attr,
-                       "position": position,
-                       "allowDuplicates": allowDuplicates,
-                       "commitComment": commitComment}
-        result = self.conn.invoke_method("POST", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute
-
-    def getAttributes(self,
-                      scheme,
-                      identifier,
-                      attrs):
-        """
-        Get one or more (possibly multi-valued) attributes of a person. The
-        returned attributes are sorted by attribute scheme precedence and
-        then attribute precedence.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/get-attributes?attrs=... ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `attrs` : str
-            [required] The attribute scheme(s) to fetch. This may
-            include any number of the attributes or pseudo-attributes, but it
-            may not include references or attribute chains (see the documentation
-            for the `fetch` parameter in this class).
-
-        **Returns**
-          list of :any:`IbisAttribute`
-            The requested attributes.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/get-attributes"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"attrs": attrs}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attributes
-
-    def getGroups(self,
-                  scheme,
-                  identifier,
-                  fetch=None):
-        """
-        Get all the groups to which the specified person belongs, including
-        indirect group memberships, via groups that include other groups.
-        The returned list of groups is sorted by groupid.
-
-        Note that some group memberships may not be visible to you. This
-        method will only return those group memberships that you have
-        permission to see.
-
-        By default, only a few basic details about each group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each group.
-
-        .. note::
-          This method will not include cancelled groups.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/groups ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisGroup`
-            The person's groups (in groupid order).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/groups"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.groups
-
-    def getInsts(self,
-                 scheme,
-                 identifier,
-                 fetch=None):
-        """
-        Get all the institutions to which the specified person belongs. The
-        returned list of institutions is sorted by name.
-
-        By default, only a few basic details about each institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references of each institution.
-
-        .. note::
-          This method will not include cancelled institutions.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/insts ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisInstitution`
-            The person's institutions (in name order).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/insts"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institutions
-
-    def isMemberOfGroup(self,
-                        scheme,
-                        identifier,
-                        groupid):
-        """
-        Test if the specified person is a member of the specified group.
-
-        .. note::
-          This may be used with cancelled people and groups.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/is-member-of-group/{groupid} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `groupid` : str
-            [required] The ID or name of the group.
-
-        **Returns**
-          bool
-            :any:`True` if the specified person is in the specified
-            group, :any:`False` otherwise (or if the person or group does not
-            exist).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/is-member-of-group/%(groupid)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier,
-                       "groupid": groupid}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.value and result.value.lower() == "true"
-
-    def isMemberOfInst(self,
-                       scheme,
-                       identifier,
-                       instid):
-        """
-        Test if the specified person is a member of the specified institution.
-
-        .. note::
-          This may be used with cancelled people and institutions, but
-          it will not include cancelled membership groups.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/is-member-of-inst/{instid} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `instid` : str
-            [required] The ID of the institution.
-
-        **Returns**
-          bool
-            :any:`True` if the specified person is in the specified
-            institution, :any:`False` otherwise (or if the person or institution
-            does not exist).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/is-member-of-inst/%(instid)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier,
-                       "instid": instid}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.value and result.value.lower() == "true"
-
-    def getManagedGroups(self,
-                         scheme,
-                         identifier,
-                         fetch=None):
-        """
-        Get all the groups that the specified person has persmission to edit.
-        The returned list of groups is sorted by groupid.
-
-        Note that some group memberships may not be visible to you. This
-        method will only include groups for which you have persmission to
-        see the applicable manager group memberships.
-
-        By default, only a few basic details about each group are returned,
-        but the optional `fetch` parameter may be used to fetch
-        additional attributes or references of each group.
-
-        .. note::
-          This method will not include cancelled groups.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/manages-groups ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisGroup`
-            The groups that the person manages (in groupid order).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/manages-groups"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.groups
-
-    def getManagedInsts(self,
-                        scheme,
-                        identifier,
-                        fetch=None):
-        """
-        Get all the institutions that the specified person has permission to
-        edit. The returned list of institutions is sorted by name.
-
-        Note that some group memberships may not be visible to you. This
-        method will only include institutions for which you have permission
-        to see the applicable editor group memberships.
-
-        By default, only a few basic details about each institution are
-        returned, but the optional `fetch` parameter may be used
-        to fetch additional attributes or references of each institution.
-
-        .. note::
-          This method will not include cancelled institutions.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/manages-insts ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `fetch` : str
-            [optional] A comma-separated list of any additional
-            attributes or references to fetch.
-
-        **Returns**
-          list of :any:`IbisInstitution`
-            The institutions that the person manages (in name order).
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/manages-insts"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier}
-        query_params = {"fetch": fetch}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.institutions
-
-    def deleteAttribute(self,
-                        scheme,
-                        identifier,
-                        attrid,
-                        commitComment=None):
-        """
-        Delete an attribute of a person. It is not an error if the attribute
-        does not exist.
-
-        Note that in this method, the `commitComment` is passed
-        as a query parameter, rather than as a form parameter, for greater
-        client compatibility.
-
-        ``[ HTTP: DELETE /api/v1/person/{scheme}/{identifier}/{attrid} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person to udpate
-            (typically their CRSid).
-
-          `attrid` : long
-            [required] The ID of the attribute to delete.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          bool
-            :any:`True` if the attribute was deleted by this method, or
-            :any:`False` if it did not exist.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/%(attrid)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier,
-                       "attrid": attrid}
-        query_params = {"commitComment": commitComment}
-        form_params = {}
-        result = self.conn.invoke_method("DELETE", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.value and result.value.lower() == "true"
-
-    def getAttribute(self,
-                     scheme,
-                     identifier,
-                     attrid):
-        """
-        Get a specific attribute of a person.
-
-        ``[ HTTP: GET /api/v1/person/{scheme}/{identifier}/{attrid} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person (typically
-            their CRSid).
-
-          `attrid` : long
-            [required] The ID of the attribute to fetch.
-
-        **Returns**
-          :any:`IbisAttribute`
-            The requested attribute.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/%(attrid)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier,
-                       "attrid": attrid}
-        query_params = {}
-        form_params = {}
-        result = self.conn.invoke_method("GET", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute
-
-    def updateAttribute(self,
-                        scheme,
-                        identifier,
-                        attrid,
-                        attr,
-                        commitComment=None):
-        """
-        Update an attribute of a person.
-
-        The attribute's value, binaryData, comment, instid and effective date
-        fields will all be updated using the data supplied. All other fields
-        will be left unchanged.
-
-        To avoid inadvertently changing fields of the attribute, it is
-        recommended that :any:`getAttribute() <PersonMethods.getAttribute>` be used to
-        retrieve the current value of the attribute, before calling this
-        method with the required changes.
-
-        ``[ HTTP: PUT /api/v1/person/{scheme}/{identifier}/{attrid} ]``
-
-        **Parameters**
-          `scheme` : str
-            [required] The person identifier scheme. Typically this
-            should be ``"crsid"``, but other identifier schemes may be
-            available in the future, such as ``"usn"`` or
-            ``"staffNumber"``.
-
-          `identifier` : str
-            [required] The identifier of the person to udpate
-            (typically their CRSid).
-
-          `attrid` : long
-            [required] The ID of the attribute to update.
-
-          `attr` : :any:`IbisAttribute`
-            [required] The new attribute values to apply.
-
-          `commitComment` : str
-            [recommended] A short textual description of
-            the change made (will be visible on the history tab in the web
-            application).
-
-        **Returns**
-          :any:`IbisAttribute`
-            The updated attribute.
-        """
-        path = "api/v1/person/%(scheme)s/%(identifier)s/%(attrid)s"
-        path_params = {"scheme": scheme,
-                       "identifier": identifier,
-                       "attrid": attrid}
-        query_params = {}
-        form_params = {"attr": attr,
-                       "commitComment": commitComment}
-        result = self.conn.invoke_method("PUT", path, path_params,
-                                         query_params, form_params)
-        if result.error:
-            raise IbisException(result.error)
-        return result.attribute