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.
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.