Repository URL to install this package:
|
Version:
1.1.5 ▾
|
kayako
/
api.py
|
|---|
# -*- coding: utf-8 -*-
#-----------------------------------------------------------------------------
# Copyright (c) 2011, Evan Leis
#
# Distributed under the terms of the Lesser GNU General Public License (LGPL)
#-----------------------------------------------------------------------------
'''
Created on May 5, 2011
@author: evan
'''
import base64
import hashlib
import hmac
import logging
import random
import urllib
import urllib2
import time
from datetime import datetime
from lxml import etree
from kayako.exception import KayakoRequestError, KayakoResponseError, KayakoInitializationError
from kayako.core.lib import FOREVER
from kayako.objects.ticket import Ticket
log = logging.getLogger('kayako')
class KayakoAPI(object):
'''
Python API wrapper for Kayako 4.01.240
--------------------------------------
**Usage:**
Set up the API::
>>> from kayako import KayakoAPI,
>>> API_URL = 'http://example.com/api/index.php'
>>> API_KEY = 'abc3r4f-alskcv3-kvj4'
>>> SECRET_KEY = 'vkl239vLKMNvlik42fv9IsflkFJlkckfjs'
>>> api = KayakoAPI(API_URL, API_KEY, SECRET_KEY)
Add a department::
>>> from kayako import Department
>>>
>>> # The following is equivalent to: department = api.create(Department, title='Customer Support Department', type='public', module='tickets'); department.add()
>>> department = api.create(Department)
>>> department.title = 'Customer Support Department'
>>> department.type = 'public'
>>> department.module = 'tickets'
>>> department.add()
>>> department.id
84
>>>
>>> # Cool, we now have a ticket department.
>>> # Lets say we want to make it private now
>>>
>>> department.type = 'private'
>>> department.save()
>>>
>>> # Ok, great. Lets delete this test object.
>>>
>>> department.delete()
>>> department.id
UnsetParameter()
>>>
>>> # We can re-add it if we want to...
>>>
>>> department.save()
>>> department.id
85
>>>
>>> # Lets see all of our Departments (yours will vary.)
>>> for dept in api.get_all(Department):
... print dept
...
<Department (1): General/tickets>
<Department (2): Suggest A Store/tickets>
<Department (3): Report A Bug/tickets>
<Department (4): Sales/livechat>
<Department (5): Commissions/livechat>
<Department (6): Missing Order/livechat>
<Department (7): Suggest A Feature/tickets>
<Department (8): Other/livechat>
<Department (49): Offers/tickets>
<Department (85): Customer Support Department/tickets>
>>>
>>> # Lets see all of our ticket Departments
>>>
>>> for dept in api.filter(Department, module='tickets')
>>> print dept
<Department (1): General/tickets>
<Department (2): Suggest A Store/tickets>
<Department (3): Report A Bug/tickets>
<Department (7): Suggest A Feature/tickets>
<Department (49): Offers/tickets>
<Department (85): Customer Support Department/tickets>
>>>
>>> # We will use this Department in the next example so lets wait to clean up the test data...
>>> #department.delete()
Add a Staff member::
>>> from kayako import Staff, StaffGroup
>>>
>>> # You can set parameters in the create method.
>>> staff = api.create(Staff, firstname='John', lastname='Doe', email='foo@example.com', username='explodes', password='easypass332')
>>>
>>> # We need to add a Staff member to a staff group.
>>> # Lets get the first StaffGroup titled "Administrator"
>>>
>>> admin_group = api.first(StaffGroup, title="Administrator")
>>> staff.staffgroupid = admin_group.id
>>>
>>> # And save the new Staff
>>>
>>> staff.add()
>>>
>>> # We will use this Staff in the next example so lets wait to clean up the test data...
>>> #staff.delete()
Add a User::
>>> from kayako import User, UserGroup, FOREVER
>>>
>>> # What fields can we add to this User?
>>> User.__add_parameters__
['fullname', 'usergroupid', 'password', 'email', 'userorganizationid', 'salutation', 'designation', 'phone', 'isenabled', 'userrole', 'timezone', 'enabledst', 'slaplanid', 'slaplanexpiry', 'userexpiry', 'sendwelcomeemail']
>>>
>>> # Lets make a new User, but not send out a welcome email.
>>> # Lets add the User to the "Registered" user group.
>>> registered = api.first(UserGroup, title='Registered')
>>> user = api.create(User, fullname="Ang Gary", password="easypass332", email="bar@example.com", usergroupid=registered.id, sendwelcomeemail=False, phone='1-800-555-5555', userexpiry=FOREVER)
>>> user.add()
>>>
>>> # Its that easy. We will use this user in the next example so lets wait to clean up the test data...
>>> # user.delete()
Add a Ticket and a TicketNote::
>>> from kayako import TicketStatus, TicketPriority
>>>
>>> # Lets add a "Bug" Ticket to any Ticket Department, with "Open" status and "High" priority for a user. Lets use the user and department from above.
>>>
>>> bug = api.first(TicketType, title="Bug")
>>> open = api.first(TicketStatus, title="Open")
>>> high = api.first(TicketPriority, title="High")
>>>
>>> ticket = api.create(Ticket, tickettypeid=bug.id, ticketstatusid=open.id, ticketpriorityid=high.id, departmentid=department.id, userid=user.id)
>>> ticket.subject = 'I found a bug and its making me very angry.'
>>> ticket.fullname = 'Ang Gary'
>>> ticket.email = 'bar@example.com'
>>> ticket.contents = 'I am an angry customer you need to make me happy.'
>>> ticket.add()
>>>
>>> # The ticket was added, lets let the customer know that everything will be fine.
>>>
>>> print 'Thanks, %s, your inquiry with reference number %s will be answered shortly.' % (ticket.fullname, ticket.displayid)
Thanks, Ang Gary, your inquiry with reference number TOJ-838-99722 will be answered shortly.'
>>>
>>> # Lets add a note to this Ticket, using the Staff member we created above.
>>>
>>> note = api.create(TicketNote, ticketid=ticket.id, contents='Customer was hostile. Will pursue anyway as this bug is serious.')
>>> note.staffid = staff.id # Alternatively, we could do: staff.fullname = 'John Doe'
>>> note.add()
>>>
>>> # Lets say the bug is fixed, we want to let the User know.
>>>
>>> post = api.create(TicketPost, ticketid=ticket.id, subject="We fixed it.", contents="We have a patch that will fix the bug.")
>>> post.add()
>>>
>>> # Now lets add an attachment to this TicketPost.
>>>
>>> with open('/var/patches/foo.diff', 'rb') as patch:
... binary_data = patch.read()
>>>
>>> attachment = api.create(TicketAttachment, ticketid=ticket.id, ticketpostid=post.id, filename='foo.diff', filetype='application/octet-stream')
>>> attachment.set_contents(binary_data) # set_contents encodes data into base 64. get_contents decodes base64 contents into the original data.
>>> attachment.add()
>>>
>>> # Lets clean up finally.
>>> ticket.delete() # This deletes the attachment, post, and note.
>>> user.delete()
>>> staff.delete()
>>> department.delete()
**API Factory Methods:**
``api.create(Object, *args, **kwargs)``
Create and return a new ``KayakoObject`` of the type given passing in args and kwargs.
``api.get_all(Object, *args, **kwargs)``
*Get all ``KayakoObjects`` of the given type.*
*In most cases, all items are returned.*
e.x. ::
>>> api.get_all(Department)
[<Department....>, ....]
*Special Cases:*
``api.get_all(User, marker=1, maxitems=1000)``
Return all ``Users`` from userid ``marker`` with up to ``maxitems``
results (max 1000.)
``api.get_all(Ticket, departmentid, ticketstatusid=-1, ownerstaffid=-1, userid=-1)``
Return all ``Tickets`` filtered by the required argument
``departmentid`` and by the optional keyword arguments.
``api.get_all(TicketAttachment, ticketid)``
Return all ``TicketAttachments`` for a ``Ticket`` with the given ID.
``api.get_all(TicketPost, ticketid)``
Return all ``TicketPosts`` for a ``Ticket`` with the given ID.
``api.get_all(TicketCustomField, ticketid)``
Return all ``TicketCustomFieldGroups`` for a ``Ticket`` with the given ID.
Returns a ``list`` of ``TicketCustomFieldGroups``.
``api.get_all(TicketCount)``
Returns only one object: ``TicketCount`` not a ``list`` of objects.
``api.filter(Object, args=(), kwargs={}, **filter)``
Gets all ``KayakoObjects`` matching a filter.
e.x. ::
>>> api.filter(Department, args=(2), module='tickets')
[<Department module='tickets'...>, <Department module='tickets'...>, ...]
``api.first(Object, args=(), kwargs={}, **filter)``
Returns the first ``KayakoObject`` found matching a given filter.
e.x. ::
>>> api.filter(Department, args=(2), module='tickets')
<Department module='tickets'>
``api.get(Object, *args)``
*Get a ``KayakoObject`` of the given type by ID.*
e.x. ::
>>> api.get(User, 112359)
<User (112359)....>
*Special Cases:*
``api.get(TicketAttachment, ticketid, attachmentid)``
Return a ``TicketAttachment`` for a ``Ticket`` with the given ``Ticket``
ID and ``TicketAttachment`` ID. Getting a specific ``TicketAttachment``
gets a ``TicketAttachment`` with the actual attachment contents.
``api.get(TicketPost, ticketid, ticketpostid)``
Return a ``TicketPost`` for a ticket with the given ``Ticket`` ID and
``TicketPost`` ID.
``api.get(TicketNote, ticketid, ticketnoteid)``
Return a ``TicketNote`` for a ticket with the given ``Ticket`` ID and
``TicketNote`` ID.
**Object persistence methods**
``kayakoobject.add()``
*Adds the instance to Kayako.*
``kayakoobject.save()``
*Saves an existing object the instance to Kayako.*
``kayakoobject.delete()``
*Removes the instance from Kayako*
These methods can raise exceptions:
Raises ``KayakoRequestError`` if one of the following is true:
- The action is not available for the object
- A required object parameter is UnsetParameter or None (add/save)
- The API URL cannot be reached
Raises ``KayakoResponseError`` if one of the following is true:
- There is an error with the request (not HTTP 200 Ok)
- The XML is in an unexpected format indicating a possible Kayako version mismatch
**Misc API Calls**
``api.ticket_search(query, ticketid=False, contents=False, author=False, email=False, creatoremail=False, fullname=False, notes=False, usergroup=False, userorganization=False, user=False, tags=False)``
*Search tickets with a query in the specified fields*
**Changes**
*1.1.4*
- Requires Kayako 4.01.240, use 1.1.3 for Kayako 4.01.204
- ``TicketNote`` now supports get and delete
- Added ``api.ticket_search``, see Misc API Calls for details.
- Refactored ticket module into ticket package. This could cause problems
if things were not imported like ``from kayako.objects import X``
- Added ``TicketCount`` object. Use ``api.get_all(TicketCount)`` to
retrieve.
- Added ``TicketTimeTrack`` object. ``api.get_all(TicketTimeTrack, ticket.id)`` or
``api.get(TicketTimeTrack, ticket.id, ticket_time_track_id)``
- Added ``Ticket.timetracks``
**Quick Reference**
================= ====================================================================== ========================= ======= ======= =====================
Object Get All Get Add Save Delete
================= ====================================================================== ========================= ======= ======= =====================
Department Yes Yes Yes Yes Yes
Staff Yes Yes Yes Yes Yes
StaffGroup Yes Yes Yes Yes Yes
Ticket departmentid, ticketstatusid= -1, ownerstaffid= -1, userid= -1 Yes Yes Yes Yes
TicketAttachment ticketid ticketid, attachmentid Yes No Yes
TicketCustomField ticketid No No No No
TicketCount Yes No No No No
TicketNote ticketid Yes Yes No Yes
TicketPost ticketid ticketid, postid Yes No Yes
TicketPriority Yes Yes No No No
TicketStatus Yes Yes No No No
TicketTimeTrack ticketid ticketid, id Yes No Yes
TicketType Yes Yes No No No
User marker=1, maxitems=1000 Yes Yes Yes Yes
UserGroup Yes Yes Yes Yes Yes
UserOrganization Yes Yes Yes Yes Yes
================= ====================================================================== ========================= ======= ======= =====================
'''
def __init__(self, api_url, api_key, secret_key):
'''
Creates a new wrapper that will make requests to the given URL using
the authentication provided.
'''
if not api_url:
raise KayakoInitializationError('API URL not specified.')
self.api_url = api_url
if not api_key:
raise KayakoInitializationError('API Key not specified.')
self.secret_key = secret_key
if not secret_key:
raise KayakoInitializationError('Secret Key not specified.')
self.api_key = api_key
## { Communication Layer
def _sanitize_parameter(self, parameter):
'''
Sanitize a specific object.
- Convert None types to empty strings
- Convert FOREVER to '0'
- Convert lists/tuples into sanitized lists
- Convert objects to strings
'''
if parameter is None:
return ''
elif parameter is FOREVER:
return '0'
elif parameter is True:
return '1'
elif parameter is False:
return '0'
elif isinstance(parameter, datetime):
return str(int(time.mktime(parameter.timetuple())))
elif isinstance(parameter, (list, tuple, set)):
return [self._sanitize_parameter(item) for item in parameter if item not in ['', None]]
else:
return str(parameter)
def _sanitize_parameters(self, **parameters):
'''
Sanitize a dictionary of parameters for a request.
'''
result = dict()
for key, value in parameters.iteritems():
result[key] = self._sanitize_parameter(value)
return result
def _post_data(self, **parameters):
'''
Turns parameters into application/x-www-form-urlencoded format.
'''
data = None
first = True
for key, value in parameters.iteritems():
if isinstance(value, list):
if len(value):
for sub_value in value:
if first:
data = '%s[]=%s' % (key, urllib2.quote(sub_value))
first = False
else:
data = '%s&%s[]=%s' % (data, key, urllib2.quote(sub_value))
else:
if first:
data = '%s[]=' % key
first = False
else:
data = '%s&%s[]=' % (data, key)
elif first:
data = '%s=%s' % (key, urllib2.quote(value))
first = False
else:
data = '%s&%s=%s' % (data, key, urllib2.quote(value))
return data
def _generate_signature(self):
'''
Generates random salt and an encoded signature using SHA256.
'''
# Generate random 10 digit number
salt = str(random.getrandbits(32))
# Use HMAC to encrypt the secret key using the salt with SHA256
encrypted_signature = hmac.new(self.secret_key, msg=salt, digestmod=hashlib.sha256).digest()
# Encode the bytes into base 64
b64_encoded_signature = base64.b64encode(encrypted_signature)
return salt, b64_encoded_signature
def _request(self, controller, method, **parameters):
'''
Get a response from the specified controller using the given parameters.
'''
log.info('REQUEST: %s %s' % (controller, method))
salt, b64signature = self._generate_signature()
if method == 'GET':
url = '%s?e=%s&apikey=%s&salt=%s&signature=%s' % (self.api_url, urllib.quote(controller), urllib.quote(self.api_key), salt, urllib.quote(b64signature))
# Append additional query args if necessary
data = self._post_data(**self._sanitize_parameters(**parameters)) if parameters else None
if data:
url = '%s&%s' % (url, data)
request = urllib2.Request(url)
elif method == 'POST' or method == 'PUT':
url = '%s?e=%s' % (self.api_url, urllib.quote(controller))
# Auth parameters go in the body for these methods
parameters['apikey'] = self.api_key
parameters['salt'] = salt
parameters['signature'] = b64signature
data = self._post_data(**self._sanitize_parameters(**parameters))
request = urllib2.Request(url, data=data, headers={'Content-length' : len(data) if data else 0})
request.get_method = lambda: method
elif method == 'DELETE': # DELETE
url = '%s?e=%s&apikey=%s&salt=%s&signature=%s' % (self.api_url, urllib.quote(controller), urllib.quote(self.api_key), salt, urllib.quote(b64signature))
data = self._post_data(**self._sanitize_parameters(**parameters))
request = urllib2.Request(url, data=data, headers={'Content-length' : len(data) if data else 0})
request.get_method = lambda: method
else:
raise KayakoRequestError('Invalid request method: %s not supported.' % method)
log.debug('REQUEST URL: %s' % url)
log.debug('REQUEST DATA: %s' % data)
try:
response = urllib2.urlopen(request)
except urllib2.HTTPError, error:
response_error = KayakoResponseError('%s: %s' % (error, error.read()))
log.error(response_error)
raise response_error
except urllib2.URLError, error:
request_error = KayakoRequestError(error)
log.error(request_error)
raise request_error
return response
## { Persistence Layer
def create(self, object, *args, **kwargs):
'''
Create a new KayakoObject of the type given, passing in args and kwargs.
'''
return object(self, *args, **kwargs)
def get_all(self, object, *args, **kwargs):
'''
Get all Kayako Objects of the given type.
By default, all items are returned.
e.x.
>>> api.get_all(Department)
[<Department....>, ....]
Special Cases:
api.get_all(User, marker=1, maxitems=1000)
Return all Users from userid ``marker`` with up to ``maxitems``
results (max 1000.)
api.get_all(Ticket, departmentid, ticketstatusid= -1,
ownerstaffid= -1, userid= -1)
Return all Tickets filtered by the required argument
``departmentid`` and by the optional keyword arguments.
api.get_all(TicketAttachment, ticketid)
Return all TicketAttachments for a Ticket with the given ID.
api.get_all(TicketPost, ticketid)
Return all TicketPosts for a Ticket with the given ID.
'''
return object.get_all(self, *args, **kwargs)
def _match_filter(self, object, **filter):
'''
Returns whether or not every given attribute of an object is equal
to the given values.
'''
for key, value in filter.iteritems():
attr = getattr(object, key)
if isinstance(attr, list):
if value not in attr:
return False
elif attr != value:
return False
return True
def filter(self, object, args=(), kwargs={}, **filter):
'''
Gets all KayakoObjects matching a filter.
e.x.
>>> api.filter(Department, args=(2), module='tickets')
[<Department module='tickets'...>, <Department module='tickets'...>, ...]
'''
objects = self.get_all(object, *args, **kwargs)
results = []
for result in objects:
if self._match_filter(result, **filter):
results.append(result)
return results
def first(self, object, args=(), kwargs={}, **filter):
'''
Returns the first KayakoObject found matching a given filter.
e.x.
>>> api.filter(Department, args=(2), module='tickets')
<Department module='tickets'>
'''
objects = self.get_all(object, *args, **kwargs)
for result in objects:
if self._match_filter(result, **filter):
return result
def get(self, object, *args):
'''
Get a Kayako Object of the given type by ID.
e.x.
api.get(User, 112359)
>>> <User....>
Special Cases:
api.get(TicketAttachment, ticketid, attachmentid)
Return a TicketAttachment for a ticket with the given Ticket ID
and TicketAttachment ID. Getting a specific TicketAttachment
gets a TicketAttachment with the actual attachment contents.
api.get(TicketPost, ticketid, ticketpostid)
Return a TicketPost for a ticket with the given Ticket ID and
TicketPost ID.
api.get(TicketNote, ticketid, ticketnoteid)
Return a TicketNote for a ticket with the given Ticket ID and
TicketNote ID.
'''
return object.get(self, *args)
def ticket_search(self, query, ticketid=False, contents=False, author=False, email=False, creatoremail=False, fullname=False, notes=False, usergroup=False, userorganization=False, user=False, tags=False):
''' Search tickets in certain parameters for a given query.
query The Search Query
ticketid=False If True, then search the Ticket ID & Mask ID
contents=False If True, then search the Ticket Post Contents
author=False If True, then search the Full Name & Email
email=False If True, then search the Email Address (Ticket & Posts)
creatoremail=False If True, then search the Email Address (only Tickets)
fullname=False If True, then search the Full Name
notes=False If True, then search the Ticket Notes
usergroup=False If True, then search the User Group
userorganization=False If True, then search the User Organization
user=False If True, then search the User (Full Name, Email)
tags=False If True, then search the Ticket Tags
'''
response = self._request('/Tickets/TicketSearch', 'POST', query=query, ticketid=ticketid, contents=contents, author=author, email=email, creatoremail=creatoremail, fullname=fullname, notes=notes, usergroup=usergroup, userorganization=userorganization, user=user, tags=tags)
ticket_xml = etree.parse(response)
return [Ticket(self, **Ticket._parse_ticket(self, ticket_tree)) for ticket_tree in ticket_xml.findall('ticket')]
def __str__(self):
return '<KayakoAPI: %s>' % self.api_url
def __repr__(self):
return 'KayakoAPI(%s, "some_key", "some_secret")' % (self.api_url)