Skip to content
Snippets Groups Projects
lib.py 35.1 KiB
Newer Older
Robert Habermann's avatar
Robert Habermann committed
# -*- coding: utf-8 -*-
from __future__ import unicode_literals  # support both Python2 and 3
Robert Habermann's avatar
Robert Habermann committed
""" lib.py
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
PyOTRS lib
Robert Habermann's avatar
Robert Habermann committed
This code implements the PyOTRS library to provide access to the OTRS API (REST)
Robert Habermann's avatar
Robert Habermann committed
"""

import os
import json
import time
import datetime

import requests
from requests.packages.urllib3 import disable_warnings

# turn of platform insecurity warnings from urllib3
disable_warnings()

# TODO 2016-04-11 (RH) ca_cert, os.environ and proxies can not stay like this!
# path to certificate bundle and set to environment
ca_certs = os.path.abspath("/etc/ssl/certs/ca-certificates.crt")

os.environ["REQUESTS_CA_BUNDLE"] = ca_certs
os.environ["CURL_CA_BUNDLE"] = ca_certs

# disable any proxies
proxies = {"http": "", "https": "", "no": ""}


class PyOTRSError(Exception):
    def __init__(self, message):
        super(PyOTRSError, self).__init__(message)
        self.message = message


class NoBaseURL(PyOTRSError):
    pass


class NoWebServiceName(PyOTRSError):
    pass


class NoCredentials(PyOTRSError):
    pass


class ResponseJSONParseError(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class SessionIDStoreNoFileError(PyOTRSError):
    pass


class SessionIDStoreNoTimeoutError(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class SessionError(PyOTRSError):
    pass


class SessionCreateError(PyOTRSError):
    pass


class SessionIDFileError(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class TicketDynamicFieldsNotRequestedError(PyOTRSError):
Robert Habermann's avatar
Robert Habermann committed
    pass


class TicketDynamicFieldsParseError(PyOTRSError):
    pass


class TicketSearchNothingToLookFor(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class TicketSearchNumberMultipleResults(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class TicketError(PyOTRSError):
    pass


class OTRSAPIError(PyOTRSError):
    pass


class OTRSHTTPError(PyOTRSError):
    pass


Robert Habermann's avatar
Robert Habermann committed
class Article(object):
Robert Habermann's avatar
Robert Habermann committed

    def __init__(self, dct):
        for key, value in dct.items():
            self.__dict__ = dct

    def __repr__(self):
Robert Habermann's avatar
Robert Habermann committed
        return "<{0}>".format(self.__class__.__name__)
Robert Habermann's avatar
Robert Habermann committed

    @classmethod
    def _dummy(cls):
Robert Habermann's avatar
Robert Habermann committed

        Returns:

        """
        return Article({"Subject": "Dümmy Subject",
                        "Body": "Hallo Bjørn,\n[kt]\n\n -- The End",
                        "TimeUnit": 0,
                        "MimeType": "text/plain",
                        "Charset": "UTF8"})
Robert Habermann's avatar
Robert Habermann committed

    def _dummy_force_notify(cls):
Robert Habermann's avatar
Robert Habermann committed

        Returns:
        """
        return Article({"Subject": "Dümmy Subject",
                        "Body": "Hallo Bjørn,\n[kt]\n\n -- The End",
                        "TimeUnit": 0,
                        "MimeType": "text/plain",
                        "Charset": "UTF8",
                        "ForceNotificationToUserID": [1, 2]})
Robert Habermann's avatar
Robert Habermann committed
        """

        Returns:

        """
        return {"Article": self.__dict__}

    def validate(self):
        """validate data against a mapping dict - if a key is not present
        then set it with a default value according to dict
        """

        validation_map = {"Body": "API created Article Body",
                          "Charset": "UTF8",
                          "MimeType": "text/plain",
                          "Subject": "API created Article",
                          "TimeUnit": 0}

        dct = self.__dict__

        for key, value in validation_map.items():
            if not self.__dict__.get(key, None):
                dct.update({key: value})

        self.__dict__ = dct


class Attachment(object):
    """PyOTRS Attachment class """

    def __init__(self, Content=None, ContentType=None, Filename=None):
        self.Content = Content  # base64 encoded
        self.ContentType = ContentType
        self.Filename = Filename

    def __repr__(self):
        return "<{0}: {1}>".format(self.__class__.__name__, self.Filename)

    def to_dct(self):
        """represent AttachmentList and related Attachment objects as dict

        Returns:

        """
        return self.__dict__

    def _dummy(cls):
Robert Habermann's avatar
Robert Habermann committed

        Returns:

        """
        return Attachment("YmFyCg==", "text/plain", "dümmy.txt")
Robert Habermann's avatar
Robert Habermann committed

    """PyOTRS DynamicField class
    .. warning::
        DynamicField representation changed between OTRS 4 and OTRS 5.
        **PyOTRS only supports OTRS 5 style!**
    """
Robert Habermann's avatar
Robert Habermann committed
    def __init__(self, name=None, value=None, dct=None):
        if dct:
            if name or value:
                raise Exception("Please provide either \"dct\" or \"name and value\"")
Robert Habermann's avatar
Robert Habermann committed
            self.name = dct["Name"]
            self.value = dct["Value"]
Robert Habermann's avatar
Robert Habermann committed
        else:
            if name and value:
                self.name = name
                self.value = value
            else:
                raise Exception("Please provide either \"dct\" or \"name and value\"")
Robert Habermann's avatar
Robert Habermann committed
        return "<{0}: {1} --> {2} >".format(self.__class__.__name__, self.name, self.value)
Robert Habermann's avatar
Robert Habermann committed
    def to_dct(self):
        """represent DynamicField as dict
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        """
        return {"Name": self.name, "Value": self.value}
    def _dummy1(cls):
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        Returns: DynamicField
        """
        return DynamicField(name="firstname", value="Jane")

    @classmethod
    def _dummy2(cls):
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        """
Robert Habermann's avatar
Robert Habermann committed
        return DynamicField(dct={'Name': 'lastname', 'Value': 'Doe'})
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
class Ticket(object):
    """PyOTRS Ticket class """

    def __init__(self, dct):

Robert Habermann's avatar
Robert Habermann committed
        self.attachment_list = []
        self.dynamic_field_list = []

Robert Habermann's avatar
Robert Habermann committed
        for key, value in dct.items():
            if isinstance(value, dict):
                dct[key] = Ticket(value)
Robert Habermann's avatar
Robert Habermann committed
            self.__dict__ = dct

    def __repr__(self):
        return "<{0}>".format(self.__class__.__name__)
Robert Habermann's avatar
Robert Habermann committed

    @classmethod
    def create_basic(cls,
                     Title=None,
                     QueueID=None,
                     Queue=None,
                     StateID=None,
                     State=None,
                     PriorityID=None,
                     Priority=None,
Robert Habermann's avatar
Robert Habermann committed
                     **kwargs):
Robert Habermann's avatar
Robert Habermann committed

        Args:
            Title:
            QueueID:
            Queue:
            StateID:
            State:
            PriorityID:
            Priority:
            CustomerUser:
            **kwargs:

        Returns:
        """
Robert Habermann's avatar
Robert Habermann committed

        if not Title:
            raise ValueError("Title is required")

        if not Queue and not QueueID:
            raise ValueError("Either Queue or QueueID required")

        if not State and not StateID:
            raise ValueError("Either State or StateID required")

        if not Priority and not PriorityID:
            raise ValueError("Either Priority or PriorityID required")

        if not CustomerUser:
            raise ValueError("CustomerUser is required")

Robert Habermann's avatar
Robert Habermann committed
        dct = {u"Title": Title}

        if Queue:
Robert Habermann's avatar
Robert Habermann committed
        else:
Robert Habermann's avatar
Robert Habermann committed

        if State:
Robert Habermann's avatar
Robert Habermann committed
        else:
Robert Habermann's avatar
Robert Habermann committed

        if Priority:
            dct.update({"Priority": Priority})
Robert Habermann's avatar
Robert Habermann committed
        else:
            dct.update({"PriorityID": PriorityID})
Robert Habermann's avatar
Robert Habermann committed

        dct.update({"CustomerUser": CustomerUser})
Robert Habermann's avatar
Robert Habermann committed
        for key, value in dct.items():
            dct.update({key: value})

        return Ticket(dct)

        """represent Ticket objects as dict
Robert Habermann's avatar
Robert Habermann committed

        Returns:

        """
        return {"Ticket": self.__dict__}

    @classmethod
    def _dummy(cls):
Robert Habermann's avatar
Robert Habermann committed

        Returns:

        """
        return Ticket.create_basic(Queue=u"Raw",
                                   State=u"open",
                                   Priority=u"3 normal",
                                   CustomerUser="root@localhost",
                                   Title="Bäsic Ticket")
    def datetime_to_pending_time_text(datetime_obj=None):
        """datetime_to_pending_time_str

        Args:
            datetime_obj (Datetime):

        Returns:
            str: The pending time in the format required for OTRS REST interface
        """
        return {
            "Year": datetime_obj.year,
            "Month": datetime_obj.month,
            "Day": datetime_obj.day,
            "Hour": datetime_obj.hour,
            "Minute": datetime_obj.minute
        }

Robert Habermann's avatar
Robert Habermann committed

class SessionIDStore(object):
Robert Habermann's avatar
Robert Habermann committed
    """Session ID: persistently store to and retrieve from to file

    Args:
        file_path (unicode): Path on disc
        session_timeout (int): OTRS Session Timeout Value (to avoid reusing outdated session id
        session_id_value (unicode): A Session ID as str
        session_id_created (int): seconds as epoch when a session_id record was created
        session_id_expires (int): seconds as epoch when a session_id record expires

    Raises:
        SessionIDStoreNoFileError
        SessionIDStoreNoTimeoutError

    .. todo::
        Is session_timeout correct here?! Or should this just return "expires" value and caller
        needs to decide whether to use it..?!

    """
    def __init__(self, file_path=None, session_timeout=None):
        if not file_path:
            raise SessionIDStoreNoFileError("Argument file_path is required!")

        if not session_timeout:
            raise SessionIDStoreNoTimeoutError("Argument session_timeout is required!")

        self.file_path = file_path
Robert Habermann's avatar
Robert Habermann committed
        self.session_timeout = session_timeout
        self.session_id_value = None
        self.session_id_created = None
        self.session_id_expires = None

    def __repr__(self):
Robert Habermann's avatar
Robert Habermann committed
        return "<{0}: {1}>".format(self.__class__.__name__, self.file_path)

    def read(self):
        """Retrieve a stored Session ID from file

        Returns:
            Union(str, None): Retrieved Session ID or None (if none could be read)
        """
        if not os.path.isfile(self.file_path):
            return None

        if not SessionIDStore._validate_file_owner_and_permissions(self.file_path):
            return None

        with open(self.file_path, "r") as f:
            content = f.read()
        try:
            data = json.loads(content)
            self.session_id_value = data['session_id']

            self.session_id_created = datetime.datetime.utcfromtimestamp(int(data['created']))
            self.session_id_expires = (self.session_id_created +
                                       datetime.timedelta(minutes=self.session_timeout))

            if self.session_id_expires > datetime.datetime.utcnow():
                return self.session_id_value  # still valid
        except ValueError:
            return None
        except KeyError:
            return None
        except Exception as err:
            raise Exception("Exception Type: {0}: {1}".format(type(err), err))

    def write(self):
        """Write and store a Session ID to file (rw for user only)

        Returns:
            bool: True if successful, False otherwise.

        .. todo::
            (RH) Error Handling and return True/False
        """

        if os.path.isfile(self.file_path):
            if not SessionIDStore._validate_file_owner_and_permissions(self.file_path):
                raise IOError("File exists but is not ok (wrong owner/permissions)!")

        with open(self.file_path, 'w') as f:
            f.write(json.dumps({'created': str(int(time.time())),
                                'session_id': self.session_id_value}))
        os.chmod(self.file_path, 384)  # 384 is '0600'

        # TODO 2016-04-23 (RH): check this
        if not SessionIDStore._validate_file_owner_and_permissions(self.file_path):
            raise IOError("Race condition: Something happened to file during the run!")

        return True

    def delete(self):
        """remove session id file (e.g. when it only contains an invalid session id

        Raises:
            NotImplementedError

        Returns:
            bool: True if successful, False otherwise.

        .. todo::
            (RH) implement this _remove_session_id_file
        """
        raise NotImplementedError("Not yet done")

    @staticmethod
    def _validate_file_owner_and_permissions(full_file_path):
        """validate SessionIDStore file ownership and permissions

        Args:
            full_file_path (unicode): full path to file on disk

        Returns:
            bool: True if valid and correct, otherwise False

        """
        if not os.path.isfile(full_file_path):
            raise IOError("Does not exist or not a file: {0}".format(full_file_path))

        file_lstat = os.lstat(full_file_path)
        if not file_lstat.st_uid == os.getuid():
            return False

        if not file_lstat.st_mode & 0o777 == 384:
            """ check for unix permission User R+W only (0600)
            >>> oct(384)
            '0600' Python 2
            >>> oct(384)
            '0o600'  Python 3  """
            return False

        return True

Robert Habermann's avatar
Robert Habermann committed
class Client(object):
    """PyOTRS Client class - includes Session handling

    Args:
        baseurl (unicode): Base URL for OTRS System, no trailing slash e.g. http://otrs.example.com
        webservicename (unicode): OTRS REST Web Service Name
        username (unicode): Username
        password (unicode): Password
        session_id_file (unicode): Session ID path on disk, used to persistently store Session ID
Robert Habermann's avatar
Robert Habermann committed
        session_timeout (int): Session Timeout configured in OTRS (usually 28800 seconds = 8h)
        https_verify (bool): Should HTTPS certificates be verified (defaults to True)

    """
    def __init__(self,
                 baseurl,
                 webservicename,
                 username=None,
                 password=None,
                 session_id_file=None,
                 session_timeout=None,
                 https_verify=True):

        if not baseurl:
            raise NoBaseURL("Missing Baseurl (e.g. https://otrs.example.com)")
Robert Habermann's avatar
Robert Habermann committed
        self.baseurl = baseurl.rstrip("/")
Robert Habermann's avatar
Robert Habermann committed

        if not webservicename:
            raise NoWebServiceName("Missing WebServiceName (e.g. GenericTicketConnectorREST)")
        self.webservicename = webservicename

        if not session_timeout:
            self.session_timeout = 28800  # 8 hours is OTRS default
        else:
            self.session_timeout = session_timeout
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        if not session_id_file:
            self.session_id_store = SessionIDStore(file_path="/tmp/.session_id.tmp",
                                                   session_timeout=self.session_timeout)
        else:
            self.session_id_store = SessionIDStore(file_path=session_id_file,
                                                   session_timeout=self.session_timeout)

Robert Habermann's avatar
Robert Habermann committed
        self.https_verify = https_verify

        # dummy initialization
        self.operation = None
        self.http_method = None
Robert Habermann's avatar
Robert Habermann committed
        self.response = None
        self.response_json = None
Robert Habermann's avatar
Robert Habermann committed
        self.response_type = None
        self.ticket_search_result = None
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        self.ticket_list = []
        self.attachment_list = []
        self.dynamic_field_list = []

        # credentials
Robert Habermann's avatar
Robert Habermann committed
        self.username = username
        self.password = password
        self.session_id = None
Robert Habermann's avatar
Robert Habermann committed

    """
    GenericInterface::Operation::Session::SessionCreate
        Methods (public):
        * session_check_is_valid
Robert Habermann's avatar
Robert Habermann committed
        * session_create
        * session_restore_or_set_up_new  # try to get session_id from a (json) file on filesystem
    """

    def session_check_is_valid(self, session_id=None):
        """check whether self.session_id (or session_id) is currently valid
            session_id (unicode): optional If set overrides the self.session_id
Robert Habermann's avatar
Robert Habermann committed

        Raises:
            SessionError if neither self.session_id nor session_id is not set
Robert Habermann's avatar
Robert Habermann committed

        Returns:
            bool: True if valid, False otherwise.

Robert Habermann's avatar
Robert Habermann committed
        .. note::
            Calls _ticket_get_json (GET)
Robert Habermann's avatar
Robert Habermann committed
        """
        if not session_id and not self.session_id:
Robert Habermann's avatar
Robert Habermann committed
            raise SessionError("No value set for session_id!")

        if session_id:
            self.session_id = session_id

        # TODO 2016-04-13 (RH): Is there a nicer way to check whether session is valid?!
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Ticket/1".format(self)
Robert Habermann's avatar
Robert Habermann committed

        payload = {"SessionID": self.session_id}
Robert Habermann's avatar
Robert Habermann committed

        return self._ticket_get_json(url, payload)
Robert Habermann's avatar
Robert Habermann committed

    def session_create(self):
        """create new OTRS Session and store Session ID
Robert Habermann's avatar
Robert Habermann committed

        Returns:
            bool: True if successful, False otherwise.

Robert Habermann's avatar
Robert Habermann committed
        .. note::
            Uses HTTP Method: POST

Robert Habermann's avatar
Robert Habermann committed
        .. todo::
            2016-04-18 (RH): decide what session_create should return (bool or result content)
Robert Habermann's avatar
Robert Habermann committed
        """
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Session".format(self)

        payload = {
            "UserLogin": self.username,
            "Password": self.password
        }

Robert Habermann's avatar
Robert Habermann committed
        # TODO 2016-04-18 (RH): decide what to return here.. bool or result content?
        if self._session_create_json(url, payload):
            return self.response_json
Robert Habermann's avatar
Robert Habermann committed
        else:
            return None
Robert Habermann's avatar
Robert Habermann committed

    def session_restore_or_set_up_new(self):
        """Try to restore Session ID from file otherwise create new one
Robert Habermann's avatar
Robert Habermann committed

        Raises:
            SessionCreateError
            SessionIDFileError

        Returns:
            bool: True if successful, False otherwise.
        """
        # try to read session_id from file
Robert Habermann's avatar
Robert Habermann committed
        self.session_id = self.session_id_store.read()

        if self.session_id:
Robert Habermann's avatar
Robert Habermann committed
            # got one.. check whether it's still valid
            try:
                if self.session_check_is_valid():
                    print("Using valid Session ID "
                          "from ({0})".format(self.session_id_store.file_path))
Robert Habermann's avatar
Robert Habermann committed
                    return True
            except OTRSAPIError:
Robert Habermann's avatar
Robert Habermann committed
                """most likely invalid session_id so pass. Remove clear session_id_store.."""
Robert Habermann's avatar
Robert Habermann committed

        # got no (valid) session_id; clean store and try to create new one
        self.session_id = None
        self.session_id_store.write()
Robert Habermann's avatar
Robert Habermann committed
        if not self.session_create():
            raise SessionCreateError("Failed to create a Session ID!")

        # safe new created session_id to file
Robert Habermann's avatar
Robert Habermann committed
        if not self.session_id_store.write():
Robert Habermann's avatar
Robert Habermann committed
            raise SessionIDFileError("Failed to save Session ID to file!")
        else:
            print("Saved new Session ID to file: {0}".format(self.session_id_store.file_path))
Robert Habermann's avatar
Robert Habermann committed
            return True

Robert Habermann's avatar
Robert Habermann committed
    def _session_create_json(self, url, payload):
Robert Habermann's avatar
Robert Habermann committed

        Args:
Robert Habermann's avatar
Robert Habermann committed
            payload (dict):

        Raises:
            OTRSAPIError

        Returns:
            bool: True if update successful, False otherwise.

        .. note::
            Uses HTTP Method: POST
        """
        self.operation = "SessionCreate"
        self.http_method = "POST"
        self.response = self._send_request(self.http_method, url, payload, self.https_verify)
        self.response_json = self.response.json()
        self._validate_response()  # TODO
Robert Habermann's avatar
Robert Habermann committed

        return True

Robert Habermann's avatar
Robert Habermann committed
    """
    GenericInterface::Operation::Ticket::TicketCreate
Robert Habermann's avatar
Robert Habermann committed
        Methods (public):
Robert Habermann's avatar
Robert Habermann committed
        * ticket_create
    """
    def ticket_create(self,
                      ticket=None,
                      article=None,
                      attachment_list=None,
                      dynamic_field_list=None,
                      **kwargs):
        """ticket_update_by_ticket_id_set_scout_id
Robert Habermann's avatar
Robert Habermann committed

        Args:
            ticket (Ticket):
            article (Article): optional article
Robert Habermann's avatar
Robert Habermann committed
            attachment_list (list): *Attachment* objects
            dynamic_field_list (list): *DynamicField* object
            **kwargs: any regular OTRS Fields (not for Dynamic Fields!)

Robert Habermann's avatar
Robert Habermann committed
        .. todo::
            2016-04-18 (RH): decide what ticket_create should return (bool or result content)
Robert Habermann's avatar
Robert Habermann committed
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Ticket".format(self)

        payload = {"SessionID": self.session_id}

        if not ticket:
            raise TicketError("provide Ticket!")

        if not article:
            raise TicketError("provide Article!")

        payload.update(ticket.to_dct())
Robert Habermann's avatar
Robert Habermann committed

        if article:
            article.validate()
            payload.update(article.to_dct())

        if attachment_list:
Robert Habermann's avatar
Robert Habermann committed
            payload.update({"Attachment": attachment_list})
Robert Habermann's avatar
Robert Habermann committed
            payload.update({"DynamicField": dynamic_field_list})
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        # TODO 2016-04-18(RH): decide what to return here.. bool or result content? Or full new?
Robert Habermann's avatar
Robert Habermann committed
        if self._ticket_create_json(url, payload):
            return self.response_json
Robert Habermann's avatar
Robert Habermann committed
        else:
            return None
Robert Habermann's avatar
Robert Habermann committed

    def _ticket_create_json(self, url, payload):
Robert Habermann's avatar
Robert Habermann committed

        Args:
Robert Habermann's avatar
Robert Habermann committed
            payload (dict):

        Raises:
            OTRSAPIError
            ResponseJSONParseError

        Returns:
            bool: True if successful, False otherwise.
        """
        self.operation = "TicketCreate"
        self.http_method = "POST"
        self.response = self._send_request(self.http_method, url, payload, self.https_verify)
        self.response_json = self.response.json()
        self._validate_response()
Robert Habermann's avatar
Robert Habermann committed
        return True

    """
    GenericInterface::Operation::Ticket::TicketGet
Robert Habermann's avatar
Robert Habermann committed
        Methods (public):
Robert Habermann's avatar
Robert Habermann committed
        * ticket_get_by_id
        * ticket_get_by_number
    """

    def ticket_get_by_id(self, ticket_id, dynamic_fields=True, all_articles=False):
Robert Habermann's avatar
Robert Habermann committed

        Args:
            ticket_id (int): Integer value of a Ticket ID
            dynamic_fields (bool): will request OTRS to include all
                Dynamic Fields (*default: True*)
            all_articles (bool): will request OTRS to include all
                Articles (+default: False*)

        Returns:
            dict: Ticket

Robert Habermann's avatar
Robert Habermann committed
        .. todo::
            2016-04-18 (RH): decide what ticket_get_by_id should return (bool or result content)
Robert Habermann's avatar
Robert Habermann committed
        """
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Ticket/{1}".format(self, ticket_id)

        payload = {
            "SessionID": self.session_id,
            "AllArticles": int(all_articles),
            "DynamicFields": int(dynamic_fields)
        }

        # TODO 2016-04-13 (RH): decide what to return here.. bool or ticket content
        if self._ticket_get_json(url, payload):
            return self.response_json['Ticket'][0]
Robert Habermann's avatar
Robert Habermann committed
        else:
            return None
Robert Habermann's avatar
Robert Habermann committed

    def ticket_get_by_number(self, ticket_number, dynamic_fields=True, all_articles=False):
Robert Habermann's avatar
Robert Habermann committed

        Args:
            ticket_number (unicode): Integer value of a Ticket ID
Robert Habermann's avatar
Robert Habermann committed
            dynamic_fields (bool): will request OTRS to include all
                    Dynamic Fields (*default: True*)
            all_articles (bool): will request OTRS to include all
                    Articles (*default: False*)

        Returns:
            dict: Ticket
        """
Robert Habermann's avatar
Robert Habermann committed
        if isinstance(ticket_number, int):
            raise TicketError("Provide ticket_number as str/unicode. Got ticket_number as int.")
Robert Habermann's avatar
Robert Habermann committed

        result_list = self.ticket_search(TicketNumber=ticket_number)

        if len(result_list) == 0:
            return None
        elif len(result_list) == 1:
            tid = result_list[0]
            return self.ticket_get_by_id(tid,
                                         dynamic_fields=dynamic_fields,
                                         all_articles=all_articles)
        else:
Robert Habermann's avatar
Robert Habermann committed
            raise TicketSearchNumberMultipleResults("Found more that one result for "
                                                    "Ticket Number: {0}".format(ticket_number))
Robert Habermann's avatar
Robert Habermann committed

    def _ticket_get_json(self, url, payload):
Robert Habermann's avatar
Robert Habermann committed

        Args:
Robert Habermann's avatar
Robert Habermann committed
            payload (dict):

        Raises:
            OTRSAPIError
            ResponseJSONParseError

        Returns:
            bool: True if successful, False otherwise.

Robert Habermann's avatar
Robert Habermann committed
        .. note::
            Uses HTTP Method: GET
        """
        self.operation = "TicketGet"
        self.http_method = "GET"
        self.response = self._send_request(self.http_method, url, payload, self.https_verify)
        self.response_json = self.response.json()
        self._validate_response()
Robert Habermann's avatar
Robert Habermann committed

        return True

    """
    GenericInterface::Operation::Ticket::TicketSearch
Robert Habermann's avatar
Robert Habermann committed
        Methods (public):
Robert Habermann's avatar
Robert Habermann committed
        * ticket_search
        * ticket_search_full_text
Robert Habermann's avatar
Robert Habermann committed
    """

    def ticket_search(self, **kwargs):
        """Wrapper for search ticket

        Args:
            **kwargs: Arbitrary keyword arguments (Dynamic Field).

        Returns:
            list: tickets that were found as list of **str**

Robert Habermann's avatar
Robert Habermann committed
        .. note::
Robert Habermann's avatar
Robert Habermann committed
            If value of kwargs is a datetime object then this object will be
            converted to the appropriate string format for REST API.
        """
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Ticket".format(self)

        payload = {
            "SessionID": self.session_id,
        }

        if kwargs is not None:
            for key, value in kwargs.items():
                if isinstance(value, datetime.datetime):
                    value = value.strftime("%Y-%m-%d %H:%M:%S")
                payload.update({key: value})

        if len(payload) == 1:
            raise TicketSearchNothingToLookFor("Nothing specified to look for!")

        if self._ticket_search_json(url, payload):
            return self.ticket_search_result
Robert Habermann's avatar
Robert Habermann committed

    def ticket_search_full_text(self, pattern):
        """Wrapper for search ticket for full text search

        Args:
            pattern (unicode): Search pattern (a % will be added to from and end automatically)
Robert Habermann's avatar
Robert Habermann committed

        Returns:
            list: tickets that were found

        Notes:
            Following examples is a dummy doctest. In order for this to work on Python2 the
             whole docstring needs to be a (Python2) unicode string (prefixed u)
Robert Habermann's avatar
Robert Habermann committed

        Examples:
            >>> 2 + 2
            4
Robert Habermann's avatar
Robert Habermann committed
            'ab'
Robert Habermann's avatar
Robert Habermann committed

        .. warning::
            Full Text is not really working yet.

        .. note::
            Waiting for progress on OTRS Bug: http://bugs.otrs.org/show_bug.cgi?id=11981
        """
        pattern_wildcard = "%{0}%".format(pattern)

        return self.ticket_search(FullTextIndex="1",
                                  Title=pattern_wildcard,
                                  ContentSearch="OR",
                                  Subject=pattern_wildcard,
                                  Body=pattern_wildcard)

    def _ticket_search_json(self, url, payload):
Robert Habermann's avatar
Robert Habermann committed
            _search_json_ticket_data

        Args:
Robert Habermann's avatar
Robert Habermann committed
            payload (dict):

        Raises:
            OTRSAPIError
            ResponseJSONParseError

        Returns:
            bool: True if search successful, False otherwise.

Robert Habermann's avatar
Robert Habermann committed
        .. note::
            Uses HTTP Method: GET
        """
        self.operation = "TicketSearch"
        self.http_method = "GET"
        self.response = self._send_request(self.http_method, url, payload, self.https_verify)
        self.response_json = self.response.json()
        self._validate_response()
Robert Habermann's avatar
Robert Habermann committed

        return True

Robert Habermann's avatar
Robert Habermann committed
    """
    GenericInterface::Operation::Ticket::TicketUpdate
        Methods (public):
        * ticket_update
        * ticket_update_set_pending
    """
    def ticket_update(self,
                      ticket_id,
                      article=None,
Robert Habermann's avatar
Robert Habermann committed
                      attachment_list=None,
                      dynamic_field_list=None,
        """ticket_update_by_ticket_id_set_scout_id
Robert Habermann's avatar
Robert Habermann committed

        Args:
Robert Habermann's avatar
Robert Habermann committed
            ticket_id (int):
Robert Habermann's avatar
Robert Habermann committed
            article (Article):
Robert Habermann's avatar
Robert Habermann committed
            attachment_list (list): *Attachment* objects
            dynamic_field_list (list): *DynamicField* objects
            **kwargs: any regular OTRS Fields (not for Dynamic Fields!)

Robert Habermann's avatar
Robert Habermann committed
        Returns:
            dict: Response from OTRS API if successful (if Article was added new Art. ID included)
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        .. todo::
            2016-04-17 (RH): decide what ticket_update should return (bool or result content)
Robert Habermann's avatar
Robert Habermann committed
        """
        url = "{0.baseurl}/otrs/nph-genericinterface.pl/Webservice/" \
              "{0.webservicename}/Ticket/{1}".format(self, ticket_id)

        payload = {"SessionID": self.session_id}
Robert Habermann's avatar
Robert Habermann committed

        if article:
            article.validate()
            payload.update(article.to_dct())
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        if attachment_list:
            if not article:
                raise TicketError("To create an attachment an article is needed!")
Robert Habermann's avatar
Robert Habermann committed
            payload.update({"Attachment": attachment_list})
Robert Habermann's avatar
Robert Habermann committed

Robert Habermann's avatar
Robert Habermann committed
        if dynamic_field_list:
Robert Habermann's avatar
Robert Habermann committed
            payload.update({"DynamicField": dynamic_field_list})
Robert Habermann's avatar
Robert Habermann committed

        # TODO 2016-04-17 (RH): decide what to return here.. bool or result content? Or full new?