def add(self): ''' Add this TicketTimeTrack. Requires: ticketid The unique numeric identifier of the ticket. contents The ticket time tracking note contents staffid The ticket time tracking creator staff identifier worktimeline The UNIX timestamp which specifies when the work was executed billtimeline The UNIX timestamp which specifies when to bill the user timespent The time spent (in seconds). timebillable The time billable (in seconds). Optional: workerstaffid The staff identifier of the worker. If not specified, the staff user creating this entry will be considered as the worker. notecolor The Note Color ''' if self.id is not UnsetParameter: raise KayakoRequestError('Cannot add a pre-existing %s. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('timetrack') self._update_from_response(node)
def add(self): ''' Add this TicketPost. Requires: ticketid The unique numeric identifier of the ticket. subject The ticket post subject contents The ticket post contents Requires one of: userid The User ID, if the ticket post is to be created as a user. staffid The Staff ID, if the ticket post is to be created as a staff ''' if self.id is not UnsetParameter: raise KayakoRequestError( 'Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) if ('userid' not in parameters and 'staffid' not in parameters) or ( 'userid' in parameters and 'staffid' in parameters): raise KayakoRequestError( 'To add a TicketPost, just one of the following parameters must be set: userid, staffid. (id: %s)' % self.id) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('post') self._update_from_response(node)
def add(self): ''' Add this Ticket. Requires: subject The Ticket Subject fullname Full Name of creator email Email Address of creator contents The contents of the first ticket post departmentid The Department ID ticketstatusid The Ticket Status ID ticketpriorityid The Ticket Priority ID tickettypeid The Ticket Type ID At least one of these must be present: userid The User ID, if the ticket is to be created as a user. staffid The Staff ID, if the ticket is to be created as a staff Optional: ownerstaffid The Owner Staff ID, if you want to set an Owner for this ticket type The ticket type: 'default' or 'phone' ''' if self.id is not UnsetParameter: raise KayakoRequestError('Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError('Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) if 'userid' not in parameters and 'staffid' not in parameters and 'email' not in parameters: raise KayakoRequestError('To add a Ticket, at least one of the following parameters must be set: userid, staffid. (id: %s)' % self.id) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('ticket') self._update_from_response(node)
def add(self): ''' Add this Comment. Requires: kbarticleid The unique numeric identifier of the article. contents The knowledgebase article comment contents creatortype The creator type. Staff: 1, User: 2. Optional: creatorid The creator (staff or user) ID. Needed when creator type is Staff, optional when creator type is User. fullname The creator (user) full name. Needed when creator type is User and creator id (user id) is not provided. email The creator email. parentcommentid Parent comment ID (when replying to some comment). ''' if self.id is not UnsetParameter: raise KayakoRequestError( 'Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('kbarticlecomment') self._update_from_response(node)
def delete(self): if not self.id: raise KayakoRequestError( 'Cannot delete a TicketNote without being attached to a ticket. The ID of the TicketNote (id) has not been specified.' ) if not self.ticketid: raise KayakoRequestError( 'Cannot delete a TicketNote without being attached to a ticket. The ID of the Ticket (ticketid) has not been specified.' ) self._delete('%s/%s/%s/' % (self.controller, self.ticketid, self.id))
def _add(self, controller): ''' Refactored method to check required parameters before adding. Also checks self.id is an UnsetParameter. Returns the response returned by the API call. ''' if self.id is not UnsetParameter: raise KayakoRequestError('Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError('Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) return self.api._request(controller, 'POST', **parameters)
def _save(self, controller, *required_parameters): ''' Refactored method to check required parameters before saving. Also checks that 'id,' if present, is an UnsetParameter. Returns the response returned by the API call. ''' if self.id is None or self.id is UnsetParameter: raise KayakoRequestError('Cannot save a non-existent %s. Use add instead.' % self.__class__.__name__) parameters = self.save_parameters for required_parameter in self.__required_save_parameters__: if required_parameter not in parameters: raise KayakoRequestError('Cannot save %s: Missing required field: %s. (id: %s)' % (self.__class__.__name__, required_parameter, self.id)) return self.api._request(controller, 'PUT', **parameters)
def delete(self): if self.kbarticleid is None or self.kbarticleid is UnsetParameter: raise KayakoRequestError( 'Cannot delete a Attachment without being attached to a . The ID of the (kbarticleid) has not been specified.' ) self._delete('%s/%s/%s/' % (self.controller, self.kbarticleid, self.id))
def add(self): ''' Add this TroubleshooterComment. creatorid The creator (staff or user) ID. Needed when creator type is Staff, optional when creator type is User. fullname The creator (user) full name. Needed when creator type is User and creator id (user id) is not provided. email The creator email. parentcommentid Parent comment ID (when replying to some comment). Requires: troubleshooterstepid The troubleshooter step ID. contents The step contents. creatortype The creator type. Staff: 1, User: 2. ''' parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('troubleshooterstepcomment') self._update_from_response(node)
def delete(self): if self.troubleshooterstepid is None or self.troubleshooterstepid is UnsetParameter: raise KayakoRequestError( 'Cannot delete a TroubleshooterAttachment without being attached to a step. The ID of the Step (troubleshooterstepid) has not been specified.' ) self._delete('%s/%s/%s/' % (self.controller, self.troubleshooterstepid, self.id))
def add(self): ''' Add this TroubleshooterStep. displayorder The display order. allowcomments Allow comments. 0 or 1. enableticketredirection Enable ticket redirection. redirectdepartmentid The redirect department ID. tickettypeid The ticket type ID. ticketpriorityid The ticket priority ID. ticketsubject The ticket subject. stepstatus The step status. Published: 1, Draft: 2 parentstepidlist Parent steps. multiple values comma separated like 1,2,3. Requires: id The unique numeric identifier of the step. categoryid The unique numeric identifier of the category. subject The step subject. contents The step contents. staffid The staff ID. ''' parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('troubleshooterstep') self._update_from_response(node)
def _delete(self, controller): ''' Refactored method to delete an object from Kayako. ''' if self.id is UnsetParameter: raise KayakoRequestError('Cannot delete a non-existent %s. The ID of the %s to delete has not been specified.' % (self.__class__.__name__, self.__class__.__name__)) self.api._request(controller, 'DELETE') self.id = UnsetParameter
def add(self): ''' Add this article. Requires: contents The content of the Article. subject The subject of the Article. creatorid The creator staff ID. Optional: contentstext The contentstext of the Article. categories The categories of the Article. creator The creator staff. author The author of the Article. email The email of the Article creator. isedited 1 or 0 boolean that controls whether or not to content of this article is edited. editeddateline The edit timestamp of the Article. editedstaffid Edited staff ID. views The views of the Article. isfeatured 1 or 0 boolean that controls whether or not to this article is a featured. allowcomments 1 or 0 boolean that controls whether or not comments are enable to this article. totalcomments The number of comments on this article. hasattachments 1 or 0 boolean that controls whether or not article has attachments. attachments The attachments of the Article. dateline The creation time of the Article. articlestatus The status of Article, 1 for published, 2 for draft.. articlerating The rating for the Article. ratinghits The rating hits for the Article. ratingcount The rating count for the Article. ''' if self.id is not UnsetParameter: raise KayakoRequestError( 'Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('kbarticle') self._update_from_response(node)
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
def add(self): parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError('Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('newsitemcomment') self._update_from_response(node)
def add(self): ''' Add this TicketNote. Requires: ticketid The unique numeric identifier of the ticket. contents The ticket note contents Requires one: staffid The Staff ID, if the ticket is to be created as a staff. fullname The Fullname, if the ticket is to be created without providing a staff user. Example: System messages, Alerts etc. Optional: forstaffid The Staff ID, this value can be provided if you wish to restrict the note visibility to a specific staff notecolor The Note Color, for more information see note colors ''' if self.id is not UnsetParameter: raise KayakoRequestError( 'Cannot add a pre-existing %s. Use save instead. (id: %s)' % (self.__class__.__name__, self.id)) parameters = self.add_parameters for required_parameter in self.__required_add_parameters__: if required_parameter not in parameters: raise KayakoRequestError( 'Cannot add %s: Missing required field: %s.' % (self.__class__.__name__, required_parameter)) if ('fullname' not in parameters and 'staffid' not in parameters) or ( 'fullname' in parameters and 'staffid' in parameters): raise KayakoRequestError( 'To add a TicketNote, just one of the following parameters must be set: fullname, staffid. (id: %s)' % self.id) response = self.api._request(self.controller, 'POST', **parameters) tree = etree.parse(response) node = tree.find('note') self._update_from_response(node)
def delete(self): if not self.id: raise KayakoRequestError( 'Cannot delete a Comment without being attached to a article. The ID of the Comment (id) has not been specified.' ) self._delete('%s/%s/' % (self.controller, self.id))
def delete(self): if self.ticketid is None or self.ticketid is UnsetParameter: raise KayakoRequestError( 'Cannot delete a TicketTimeTrack without being attached to a ticket. The ID of the Ticket (ticketid) has not been specified.' ) self._delete('%s/%s/%s/' % (self.controller, self.ticketid, self.id))
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='*****@*****.**', username='******', password='******') >>> >>> # 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="******", email="*****@*****.**", 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 = '*****@*****.**' >>> 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* ``api.ticket_search_full(query)`` *Shorthand for ``api.ticket_search.`` Searches all 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] ] elif isinstance(parameter, unicode): return parameter.encode('utf-8', 'ignore') 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