Ejemplo n.º 1
0
    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)
Ejemplo n.º 2
0
    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)
Ejemplo n.º 3
0
	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)
Ejemplo n.º 4
0
    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)
Ejemplo n.º 5
0
 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))
Ejemplo n.º 6
0
 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)
Ejemplo n.º 7
0
 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)
Ejemplo n.º 8
0
 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)
Ejemplo n.º 12
0
 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)
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
	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)
Ejemplo n.º 16
0
    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)
Ejemplo n.º 17
0
 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))
Ejemplo n.º 18
0
 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))
Ejemplo n.º 19
0
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