...

Writing an iManage 10 Python wrapper

I've had the iManage 10 api available to me for awhile, but I haven't had the need to use it until recently after being asked about writing some SQL statements to get a user's recent matters.

We've upgraded to the iManage 10, and happily it comes with a great rest api to manage many of the things we'd use to do with SOAP calls from Intapp's Integration Builder. As we're slowly moving some more things into the Python world, being able to rely on a Python wrapper that makes handling the rest requests to the iManage API a breeze has become a necessity.

This post is going to highlight some of the lessons learned through the experience.

The IManage Class

I started with a class "IManage", and after a short bit of reading through the API, it is one of those API's that requires a login, returns a token, then that token is used through all the subsequent calls. When done, the program must call a logout method. I know I'll likely forget to do that every time I need this functionality, so we'll use the __enter__ and __exit__ methods. This will allow us to call the constructor using Python's 'with' statement.

When instantiating an instance of an option using With we can be sure the session is closed by making sure the __exit__ method of the class manages the logout. This makes for much better logic flow instead of having to login, perform the actions, logout.

So far then we have a class that looks like the following. There are some improvements to be made, however, the general highlights are here:

class IManage:

    def __init__(self, username=None):
        if not username:
            raise TypeError('A valid username must be provided')
        else:
            self.user_id = username

        self.protocol = 'https'
        self.server = os.environ.get('IMANAGE_SERVER_HOST')
        self.api_version = 'v1'
        self.x_auth_token = ''
        self.is_connected = False
        self.verify = False

    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.disconnect()

    def __del__(self):
        self.disconnect()

You'll note that __enter__ must return self if the as statement is to be used with the with. But what about those connect and disconnect methods? Those handle the calls to the api for login and logout. Each use the same base url which is also provided in the class as a little convenience function.

    def base_url(self):
        """ Creates a base url for the iManage api calls """
        return '{protocol}://{server}/api/{version}'.format(
                        protocol=self.protocol,
                        server=self.server,
                        version=self.api_version)

    def connect(self):
        """ Establishes the connection and sets the X-Auth-Token for this instance """
        login_credentials = {'user_id': self.user_id,
                             'password': os.environ.get('IMANAGE_IMPERSONATION_PW'),
                             'persona': 'user',
                             'application_name': 'Web Mobile'}

        response = requests.put('{base_url}/session/login'.format(base_url=self.base_url()),
                                json=login_credentials, verify=self.verify)
        json_response = response.json()

        if response.status_code == 200:
            self.x_auth_token = json_response['X-Auth-Token']
            self.is_connected = True
            return True
        elif response.status_code == 401:
            raise PermissionError
        else:
            return False

    def disconnect(self):
        """ Severs the current session """
        if self.is_connected:
            headers = {'X-Auth-Token': self.x_auth_token}
            requests.get('{base_url}/session/logout'.format(base_url=self.base_url()),
                         headers=headers,
                         verify=self.verify)
        self.is_connected = False

Once that is all done, we can call the services quite easily. An example would be the recent folders and my_matters. The stuff that is common to both is in the folders method and the repeating call to the API is in execute. I put it there because almost all of the calls, particular gets, will use a nearly identical pattern.

    @staticmethod
    def parameter_update(query_params=None):
        return query_params if query_params else {}

    def execute(self, sub_resource, query_params=None, method='GET'):
        """ Executes a rest call """
        if self.is_connected:
            headers = {'X-Auth-Token': self.x_auth_token}
            if method == 'GET':
                parameters = self.parameter_update(query_params)
                response = requests.get('{base_url}/{sr}'.format(base_url=self.base_url(), sr=sub_resource),
                                        headers=headers, params=parameters,
                                        verify=self.verify)
                if response.status_code == 200:
                    return response.json()
                else:
                    # todo: handle more codes
                    return {}
        else:
            return {}

    def folders(self, sub_resource=None, query_params=None, method='GET'):
        """ Handles the folders calls """
        # handle special case for folder id
        if isinstance(sub_resource, int):
            # todo: handle the put, patch, get and delete methods given a folder id
            pass
        else:
            # continue if using one of the supported sub_resources
            if sub_resource in ('my-favorites', 'my-matters/children', 'recent', 'search'):
                parameters = self.parameter_update(query_params)
                return self.execute('folders/{sr}'.format(sr=sub_resource), query_params=parameters, method=method,)
            else:
                # todo: provide better feedback
                return {}

    def folders_recent(self, query_params=None):
        """ Returns a list of folders that contain documents upon which there has been activity by the user """
        parameters = self.parameter_update(query_params)
        return self.folders(sub_resource='recent', query_params=parameters)

    def folders_my_matters(self, query_params=None):
        """ Returns the items under the user's My Matters folder """
        parameters = self.parameter_update(query_params)
        return self.folders(sub_resource='my-matters/children', query_params=parameters)

So pulling this together we can now do something like the following.

import IManage

# Sample usage
with IManage(username='some.user') as i_manage:
    # Get all recent folders where the activity was on an email
    recent_folders = i_manage.folders_recent(query_params={'email_only': 'true', })

    # Get all items under the My Matters folder
    my_matters = i_manage.folders_my_matters()

That's pretty much it for now. I'll add more features to the class as needed. I was asked why it has to be so complicated; a sql statement seems so much easier doesn't it? Well, it may now, however, as we look a few years down the road, I suspect this will be hosted off premises and direct SQL calls are not likely how we'll interact. Today we build for what the infrastructure will likely be in the future, mainly so as not to have to be the one going back and porting everything to the 'new' API.