diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 17158e592e59b73888430701254cc5083a54bdc8..63f65c49f2115994258c3392fad96088afcb7752 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,8 +13,14 @@ This project adheres to [Semantic Versioning](http://semver.org/). 0.2.0 - planned first release ----------------------------- +0.1.11 - 2016-11-03 +------------------- +- add link api +- add dynamic_field_get access +- add article_get access + 0.1.10 - 2016-09-16 ------------------- +------------------- - add testing (and *support*) for Python2.6 (default on RHEL6) 0.1.9 - 2016-09-11 diff --git a/README.rst b/README.rst index d0ceab07c186d6d6a3961e02d572f20fa5d7edf0..1b4f7a8aa7c8b43953757ba98f0f1ad470445297 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,7 @@ Some of the most notable methods provided are:: * Client.session_create (Use credentials to "log in") * Client.ticket_create * Client.ticket_get_by_list (takes a list) - * Client.ticket_get_by_number (takes a string) + * Client.ticket_get_by_id (takes an int) * Client.ticket_search * Client.ticket_update diff --git a/pyotrs/lib.py b/pyotrs/lib.py index f9490c7fd82bb29e74c89bcab2a9c9a8fdabe95d..28398d87357b1b4500bd1c0b1b953dd580588c2e 100644 --- a/pyotrs/lib.py +++ b/pyotrs/lib.py @@ -18,21 +18,45 @@ import datetime import requests OPERATION_MAPPING_DEFAULT = { - 'SessionCreate': {'RequestMethod': 'POST', 'Route': '/Session'}, - 'TicketCreate': {'RequestMethod': 'POST', 'Route': '/Ticket'}, - 'TicketGet': {'RequestMethod': 'GET', 'Route': '/Ticket/:TicketID'}, - 'TicketGetList': {'RequestMethod': 'GET', 'Route': '/TicketList'}, - 'TicketSearch': {'RequestMethod': 'GET', 'Route': '/Ticket'}, - 'TicketUpdate': {'RequestMethod': 'PATCH', 'Route': '/Ticket/:TicketID'} -} - -TYPE_MAPPING_DEFAULT = { - 'SessionCreate': 'SessionID', - 'TicketCreate': 'TicketID', - 'TicketGet': 'Ticket', - 'TicketGetList': 'Ticket', - 'TicketSearch': 'TicketID', - 'TicketUpdate': 'TicketID' + 'SessionCreate': {'RequestMethod': 'POST', + 'Route': '/Session', + 'Result': 'SessionID'}, + 'TicketCreate': {'RequestMethod': 'POST', + 'Route': '/Ticket', + 'Result': 'TicketID'}, + 'TicketGet': {'RequestMethod': 'GET', + 'Route': '/Ticket/:TicketID', + 'Result': 'Ticket'}, + 'TicketGetList': {'RequestMethod': 'GET', + 'Route': '/TicketList', + 'Result': 'Ticket'}, + 'TicketSearch': {'RequestMethod': 'GET', + 'Route': '/Ticket', + 'Result': 'TicketID'}, + 'TicketUpdate': {'RequestMethod': 'PATCH', + 'Route': '/Ticket/:TicketID', + 'Result': 'TicketID'}, + 'LinkAdd': {'RequestMethod': 'POST', + 'Route': '/LinkAdd', + 'Result': 'LinkAdd'}, + 'LinkDelete': {'RequestMethod': 'DELETE', + 'Route': '/LinkDelete', + 'Result': 'LinkDelete'}, + 'LinkDeleteAll': {'RequestMethod': 'DELETE', + 'Route': '/LinkDeleteAll', + 'Result': 'LinkDeleteAll'}, + 'LinkList': {'RequestMethod': 'GET', + 'Route': '/LinkList', + 'Result': 'LinkList'}, + 'PossibleLinkList': {'RequestMethod': 'GET', + 'Route': '/PossibleLinkList', + 'Result': 'PossibleLinkList'}, + 'PossibleObjectsList': {'RequestMethod': 'GET', + 'Route': '/PossibleObjectsList', + 'Result': 'PossibleObject'}, + 'PossibleTypesList': {'RequestMethod': 'GET', + 'Route': '/PossibleTypesList', + 'Result': 'PossibleType'} } @@ -676,13 +700,16 @@ class Client(object): password (str): Password session_id_file (str): Session ID path on disc, used to persistently store Session ID session_timeout (int): Session Timeout configured in OTRS (usually 28800 seconds = 8h) + session_validation_ticket_id (int): Ticket ID of an existing ticket - used to perform + several check - e.g. validate log in (defaults to 1) + webservicename_link (str): OTRSGenericInterfaceLinkMechanism REST Web Service Name proxies (dict): Proxy settings - refer to requests docs for more information - default to no proxies https_verify (bool): Should HTTPS certificates be verified (defaults to True) ca_cert_bundle (str): file path - if specified overrides python/system default for Root CA bundle that will be used. - operation_map (dict): A dictionary for mapping OTRS operations to HTTP Method and Route. - type_map (dict): A dictionary for mapping OTRS result type to operations. + operation_map (dict): A dictionary for mapping OTRS operations to HTTP Method, Route and + Result string. """ def __init__(self, @@ -690,14 +717,14 @@ class Client(object): webservicename=None, username=None, password=None, - session_timeout=None, session_id_file=None, + session_timeout=None, session_validation_ticket_id=1, + webservicename_link=None, proxies=None, https_verify=True, ca_cert_bundle=None, - operation_map=None, - type_map=None): + operation_map=None): if not baseurl: raise ArgumentMissingError("baseurl") @@ -707,6 +734,10 @@ class Client(object): raise ArgumentMissingError("webservicename") self.webservicename = webservicename + # TODO 2016-30-10 (RH): check Name and Documentation + if not webservicename_link: + self.webservicename_link = "GenericLinkConnectorREST" + if not session_timeout: self.session_timeout = 28800 # 8 hours is OTRS default else: @@ -742,11 +773,6 @@ class Client(object): else: self.operation_map = OPERATION_MAPPING_DEFAULT - if type_map: - self.type_map = type_map - else: - self.type_map = TYPE_MAPPING_DEFAULT - # credentials self.username = username self.password = password @@ -1182,6 +1208,264 @@ class Client(object): return self.ticket_update(ticket_id, State=new_state, PendingTime=pt) + """ + GenericInterface::Operation::Link::LinkAdd + * link_add + """ + def link_add(self, + src_object_id, + dst_object_id, + src_object_type="Ticket", + dst_object_type="Ticket", + link_type="Normal", + state="Valid"): + """link_add + + Args: + src_object_id (int): Integer value of source object ID + dst_object_id (int): Integer value of destination object ID + src_object_type (str): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + dst_object_type (str): Object type of destination; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + link_type (str): Type of the link: "Normal" or "ParentChild" (*default: Normal*) + state (str): State of the link (*default: Normal*) + + Returns: + **True** or **False**: True if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "LinkAdd" + + payload = { + "SessionID": self.session_id_store.value, + "SourceObject": src_object_type, + "SourceKey": int(src_object_id), + "TargetObject": dst_object_type, + "TargetKey": int(dst_object_id), + "Type": link_type, + "State": state + } + + return self._parse_and_validate_response(self._send_request(payload)) + + """ + GenericInterface::Operation::Link::LinkDelete + * link_delete + """ + def link_delete(self, + src_object_id, + dst_object_id, + src_object_type="Ticket", + dst_object_type="Ticket", + link_type="Normal"): + """link_delete + + Args: + src_object_id (int): Integer value of source object ID + src_object_type (str): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + dst_object_id (int): Integer value of source object ID + dst_object_type (str): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + link_type (str): Type of the link: "Normal" or "ParentChild" (*default: Normal*) + + Returns: + **True** or **False**: True if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "LinkDelete" + + payload = { + "SessionID": self.session_id_store.value, + "Object1": src_object_type, + "Key1": int(src_object_id), + "Object2": dst_object_type, + "Key2": int(dst_object_id), + "Type": link_type + } + + return self._parse_and_validate_response(self._send_request(payload)) + + """ + GenericInterface::Operation::Link::LinkDeleteAll + * link_delete_all + """ + def link_delete_all(self, + object_id, + object_type="Ticket",): + """link_delete_all + + Args: + object_id (int): Integer value of source object ID + object_type (str): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + + Returns: + **True** or **False**: True if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "LinkDeleteAll" + + payload = { + "SessionID": self.session_id_store.value, + "Object": object_type, + "Key": int(object_id) + } + + return self._parse_and_validate_response(self._send_request(payload)) + + """ + GenericInterface::Operation::Link::LinkList + * link_list + """ + def link_list(self, + src_object_id, + src_object_type="Ticket", + dst_object_type=None, + state="Valid", + link_type=None, + direction=None): + """link_list + + Args: + src_object_id (int): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + src_object_type (str): Object type of destination; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + dst_object_type (str): Object type of destination; e.g. "Ticket", "FAQ"... + Optional restriction of the object where the links point to. (*default: Ticket*) + state (str): State of the link (*default: Valid*) + link_type (str): Type of the link: "Normal" or "ParentChild" (*default: Normal*) + direction (str): Optional restriction of the link direction ('Source' or 'Target'). + + Returns: + **Dict** or **None**: Dict if successful, if empty **None**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "LinkList" + + payload = { + "SessionID": self.session_id_store.value, + "Object": src_object_type, + "Key": int(src_object_id), + "State": state + } + + if dst_object_type: + payload.update({"Object2": dst_object_type}) + + if link_type: + payload.update({"Type": link_type}) + + if direction: + payload.update({"Direction": direction}) + + return self._parse_and_validate_response(self._send_request(payload)) + + """ + GenericInterface::Operation::Link::PossibleLinkList + * link_possible_link_list + """ + def link_possible_link_list(self): + """link_possible_link_list + + Returns: + **List** or **False**: List if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "PossibleLinkList" + + payload = { + "SessionID": self.session_id_store.value, + } + + if self._parse_and_validate_response(self._send_request(payload)): + return self.result + else: + return False + + """ + GenericInterface::Operation::Link::PossibleObjectsList + * link_possible_objects_list + """ + def link_possible_objects_list(self, + object_type="Ticket"): + """link_possible_objects_list + + Args: + object_type (str): Object type; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + + Returns: + **List** or **False**: List if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "PossibleObjectsList" + + payload = { + "SessionID": self.session_id_store.value, + "Object": object_type, + } + + if self._parse_and_validate_response(self._send_request(payload)): + return self.result + else: + return False + + """ + GenericInterface::Operation::Link::PossibleTypesList + * link_possible_types_list + """ + def link_possible_types_list(self, + src_object_type="Ticket", + dst_object_type="Ticket"): + """link_possible_types_list + + Args: + src_object_type (str): Object type of source; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + dst_object_type (str): Object type of destination; e.g. "Ticket", "FAQ"... + (*default: Ticket*) + + Returns: + **List** or **False**: List if successful, otherwise **False**. + + """ + if not self.session_id_store.value: + raise SessionNotCreated("Call session_create() or " + "session_restore_or_set_up_new() first") + self.operation = "PossibleTypesList" + + payload = { + "SessionID": self.session_id_store.value, + "Object1": src_object_type, + "Object2": dst_object_type, + } + + if self._parse_and_validate_response(self._send_request(payload)): + return self.result + else: + return False + def _build_url(self, ticket_id=None): """build url for request @@ -1194,7 +1478,7 @@ class Client(object): """ route = self.operation_map[self.operation]["Route"] - if not (route.startswith("/Ticket") or route.startswith("/Session")): + if not (route.startswith(("/Ticket", "/Session", "/Possible", "/Link"))): raise ValueError("Route misconfigured: {0}".format(route)) if ":" in route: @@ -1207,16 +1491,29 @@ class Client(object): raise ValueError("TicketID is None but Route requires " "TicketID: {0}".format(route)) - self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" - "{1}{2}{3}".format(self.baseurl, - self.webservicename, - route, - ticket_id)) + if route.startswith(("/Link", "/Possible")): + self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" + "{1}{2}{3}".format(self.baseurl, + self.webservicename_link, + route, + ticket_id)) + else: + self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" + "{1}{2}{3}".format(self.baseurl, + self.webservicename, + route, + ticket_id)) else: - self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" - "{1}{2}".format(self.baseurl, - self.webservicename, - route)) + if route.startswith(("/Link", "/Possible")): + self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" + "{1}{2}".format(self.baseurl, + self.webservicename_link, + route)) + else: + self._url = ("{0}/otrs/nph-genericinterface.pl/Webservice/" + "{1}{2}".format(self.baseurl, + self.webservicename, + route)) return self._url @@ -1234,18 +1531,18 @@ class Client(object): **requests.Response**: Response received after sending the request. .. note:: - Supported HTTP Methods: GET, HEAD, PATCH, POST, PUT + Supported HTTP Methods: DELETE, GET, HEAD, PATCH, POST, PUT """ if not payload: raise ArgumentMissingError("payload") - self._result_type = self.type_map[self.operation] + self._result_type = self.operation_map[self.operation]["Result"] url = self._build_url(ticket_id) http_method = self.operation_map[self.operation]["RequestMethod"] - if http_method not in ["GET", "HEAD", "PATCH", "POST", "PUT"]: + if http_method not in ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"]: raise ValueError("invalid http_method") headers = {"Content-Type": "application/json"} @@ -1317,6 +1614,19 @@ class Client(object): self.result = self.result_json['TicketID'] return True + # handle Link operations; Add, Delete, DeleteAll return: {"Success":1} + if self.operation in ["LinkAdd", "LinkDelete", "LinkDeleteAll"]: + if self.result_json.get("Success", None) == 1: # TODO 2016-10-30 (RH): enough?! + return True + + # LinkList result can be empty + if self.operation in "LinkList": + _link_list = self.result_json.get("LinkList", None) + if _link_list == u'': + return None + if len(_link_list): + return _link_list + # now handle other operations if self.result_json.get(self._result_type, None): self._result_error = False