From 7ee2445e53a73bbf361e2680cbe8625f90b24ce6 Mon Sep 17 00:00:00 2001 From: Michael Ducharme Date: Tue, 5 Jul 2016 21:34:58 -0500 Subject: [PATCH 1/9] Restructured into modules for easier extension (configitems, faq, etc) Made numerous updates to restructure this to allow adding further web service modules (GenericConfigItemConnectorSOAP, the FAQ connector, etc) --- README.rst | 29 ++-- otrs/client.py | 326 +++++++++++++++++--------------------- otrs/objects.py | 70 ++------ otrs/ticket/__init__.py | 0 otrs/ticket/objects.py | 54 +++++++ otrs/ticket/operations.py | 209 ++++++++++++++++++++++++ otrs/ticket/template.py | 5 + 7 files changed, 445 insertions(+), 248 deletions(-) create mode 100644 otrs/ticket/__init__.py create mode 100644 otrs/ticket/objects.py create mode 100644 otrs/ticket/operations.py create mode 100644 otrs/ticket/template.py diff --git a/README.rst b/README.rst index fc9af8c..32e70a3 100644 --- a/README.rst +++ b/README.rst @@ -30,27 +30,30 @@ Install Using ----- -First make sure you installed the ``GenericTicketConnector`` webservice, +First make sure you installed the ``GenericTicketConnectorSOAP`` webservice, see `official documentation`_. +Note: in older versions of OTRS, GenericTicketConnectorSOAP was called GenericTicketConnector + :: - from otrs.client import GenericTicketConnector - from otrs.objects import Ticket, Article, DynamicField, Attachment + from otrs.ticket.template import GenericTicketConnectorSOAP + from otrs.client import GenericInterfaceClient + from otrs.ticket.objects import Ticket, Article, DynamicField, Attachment server_uri = r'https://otrs.example.net' - webservice_name = 'GenericTicketConnector' - client = GenericTicketConnector(server_uri, webservice_name) + webservice_name = 'GenericTicketConnectorSOAP' + client = GenericInterfaceClient(server_uri, tc=GenericTicketConnectorSOAP(webservice_name)) Then authenticate, you have three choices : :: # user session - client.user_session_register('login', 'password') + client.tc.SessionCreate(user_login='login', password='password') # customer_user session - client.customer_user_session_register('login' , 'password') + client.tc.SessionCreate(customer_user_login='login' , password='password') # save user in memory client.register_credentials(user='login', 'password') @@ -79,7 +82,7 @@ Create a ticket : ContentType=mimetype, Filename="image001.png") att_file.close() - t_id, t_number = client.ticket_create(t, a, [df1, df2], [att1]) + t_id, t_number = client.tc.TicketCreate(t, a, [df1, df2], [att1]) Update an article : @@ -87,30 +90,30 @@ Update an article : # changes the title of the ticket t_upd = Ticket(Title='Updated ticket') - client.ticket_update(t_id, t_upd) + client.tc.TicketUpdate(t_id, t_upd) # appends a new article (attachments optional) new_article = Article(Subject='Moar info', Body='blabla', Charset='UTF8', MimeType='text/plain') - client.update_ticket(article=new_article, attachments=None) + client.tc.TicketUpdate(article=new_article, attachments=None) Search for tickets : :: # returns all the tickets of customer 42 - tickets = client.ticket_search(CustomerID=42) + tickets = client.tc.TicketSearch(CustomerID=42) # returns all tickets in queue Support # for which Dynamic Field 'Project' starts with 'Pizza': df2 = DynamicField(Name='Project', Value='Pizza%', Operator="Like") - client.ticket_search(Queues='Support', dynamic_fields=[df_search]) + client.tc.TicketSearch(Queues='Support', dynamic_fields=[df_search]) Retrieve a ticket : :: - ticket = client.ticket_get(138, get_articles=True, get_dynamic_fields=True, get_attachments=True) + ticket = client.tc.TicketGet(138, get_articles=True, get_dynamic_fields=True, get_attachments=True) article = ticket.articles()[0] article.save_attachments(r'C:\temp') diff --git a/otrs/client.py b/otrs/client.py index 339589c..4811e95 100644 --- a/otrs/client.py +++ b/otrs/client.py @@ -4,10 +4,10 @@ import urllib2 from posixpath import join as urljoin import xml.etree.ElementTree as etree -from .objects import Ticket, OTRSObject, DynamicField, extract_tagname +from .objects import OTRSObject, extract_tagname import codecs import sys - +import abc class OTRSError(Exception): def __init__(self, fd): @@ -64,44 +64,56 @@ def add_auth(self, *args, **kwargs): return add_auth +class OperationBase(object): + """ Base class for OTRS operations -SOAP_ENVELOPPE = """ - - - {} - -""" + """ + __metaclass__ = abc.ABCMeta + def __init__(self, opName): + self.operName = opName # otrs connector operation name + self.wsObject = None # web services object this operation belongs to -class GenericTicketConnector(object): - """ Client for the GenericTicketConnector SOAP API + def __init__(self): + self.operName = type(self).__name__ # otrs connector operation name + self.wsObject = None # web services object this operation belongs to - see http://otrs.github.io/doc/manual/admin/3.3/en/html/genericinterface.html - """ + def getWebServiceObjectAttribute(self, attribName): + return getattr(self.wsObject, attribName) - def __init__(self, server, webservice_name='GenericTicketConnector', ssl_context=None): - """ @param server : the http(s) URL of the root installation of OTRS - (e.g: https://tickets.example.net) + def getClientObjectAttribute(self, attribName): + return self.wsObject.getClientObjectAttribute(attribName) - @param webservice_name : the name of the installed webservice - (choosen by the otrs admin). - """ + def setClientObjectAttribute(self, attribName, attribValue): + self.wsObject.setClientObjectAttribute(attribName, attribValue) - self.endpoint = urljoin( - server, 'otrs/nph-genericinterface.pl/Webservice/', - webservice_name) - self.login = None - self.password = None - self.session_id = None - self.ssl_context = ssl_context + @abc.abstractmethod + def __call__(self): + return - def register_credentials(self, login, password): - """ Save the identifiers in memory, they will be used with each - subsequent request requiring authentication - """ - self.login = login - self.password = password + @property + def endpoint(self): + return self.getWebServiceObjectAttribute('endpoint') + + @property + def login(self): + return self.getClientObjectAttribute('login') + + @property + def password(self): + return self.getClientObjectAttribute('password') + + @property + def session_id(self): + return self.getClientObjectAttribute('session_id') + + @session_id.setter + def session_id(self, sessionid): + self.setClientObjectAttribute('session_id', sessionid) + + @property + def soap_envelope(self): + return '{}' def req(self, reqname, *args, **kwargs): """ Wrapper arround a SOAP request @@ -174,185 +186,139 @@ def _unpack_resp_one(element): """ return element.getchildren()[0].getchildren()[0].getchildren()[0] - @staticmethod - def _pack_req(element): + def _pack_req(self, element): """ @param element : a etree.Element @returns : a string, wrapping element within the request tags """ - return SOAP_ENVELOPPE.format(codecs.decode(etree.tostring(element),'utf-8')).encode('utf-8') + return self.soap_envelope.format(codecs.decode(etree.tostring(element),'utf-8')).encode('utf-8') + +class WebService(object): + """ Base class for OTRS Web Service + + """ + + def __init__(self, wsName, wsNamespace, **kwargs): + self.clientObject = None # link to parent client object + self.wsName = wsName # name for OTRS web service + self.wsNamespace = wsNamespace # OTRS namespace url + + # add all variables in kwargs into the local dictionary + self.__dict__.update(kwargs) + + # for operations, set backlinks to their associated webservice + for arg in kwargs: + if isinstance(getattr(self, arg), OperationBase): + getattr(self, arg).wsObject = self + # if attribute is type OperationBase, set backlink to WebService + + # set defaults if attributes are not present + if not hasattr(self, 'wsRequestNameScheme'): + self.wsRequestNameScheme = 'DATA' + if not hasattr(self, 'wsResponseNameScheme'): + self.wsResponseNameScheme = 'DATA' + + def getClientObjectAttribute(self, attribName): + return getattr(self.clientObject,attribName) + + def setClientObjectAttribute(self, attribName, attribValue): + setattr(self.clientObject,attribName,attribValue) + + @property + def endpoint(self): + return urljoin(self.getClientObjectAttribute('giurl'),self.wsName) + + +class GenericInterfaceClient(object): + """ Client for the OTRS Generic Interface + + """ + + def __init__(self, server, **kwargs): + """ @param server : the http(s) URL of the root installation of OTRS + (e.g: https://tickets.example.net) + """ + + # add all variables in kwargs into the local dictionary + self.__dict__.update(kwargs) + + # for webservices attached to this client object, backlink them + # to this client object to allow access to session login/password + + for arg in kwargs: + if isinstance(getattr(self, arg), WebService): + getattr(self, arg).clientObject = self # set backlink for web services to this obj + self.login = None + self.password = None + self.session_id = None + self.giurl = urljoin( + server, 'otrs/nph-genericinterface.pl/Webservice/') + + def register_credentials(self, login, password): + """ Save the identifiers in memory, they will be used with each + subsequent request requiring authentication + """ + self.login = login + self.password = password + +class OldGTCClass(GenericInterfaceClient): + """ Old deprecated generic ticket connector class, used for + backward compatibility with previous versions. All + methods in here are deprecated + """ def session_create(self, password, user_login=None, customer_user_login=None): - """ Logs the user or customeruser in + """ DEPRECATED - Logs the user or customeruser in @returns the session_id """ - if user_login: - ret = self.req('SessionCreate', - UserLogin=user_login, - Password=password) - else: - ret = self.req('SessionCreate', - CustomerUserLogin=customer_user_login, - Password=password) - signal = self._unpack_resp_one(ret) - session_id = signal.text - return session_id + self.tc.SessionCreate(password,user_login=user_login,customer_user_login=customer_user_login) def user_session_register(self, user, password): """ Logs the user in and stores the session_id for subsequent requests + DEPRECATED """ - self.session_id = self.session_create( + self.session_create( password=password, user_login=user) def customer_user_session_register(self, user, password): """ Logs the customer_user in and stores the session_id for subsequent - requests. + requests. DEPRECATED """ - self.session_id = self.session_create( + self.session_create( password=password, customer_user_login=user) @authenticated - def ticket_get(self, ticket_id, get_articles=False, - get_dynamic_fields=False, - get_attachments=False, *args, **kwargs): - """ Get a ticket by id ; beware, TicketID != TicketNumber - - @param ticket_id : the TicketID of the ticket - @param get_articles : grab articles linked to the ticket - @param get_dynamic_fields : include dynamic fields in result - @param get_attachments : include attachments in result - - @return a `Ticket`, Ticket.articles() will give articles if relevant. - Ticket.articles()[i].attachments() will return the attachments for - an article, wheres Ticket.articles()[i].save_attachments() - will save the attachments of article[i] to the specified folder. - """ - params = {'TicketID': str(ticket_id)} - params.update(kwargs) - if get_articles: - params['AllArticles'] = True - if get_dynamic_fields: - params['DynamicFields'] = True - if get_attachments: - params['Attachments'] = True - - ret = self.req('TicketGet', **params) - return Ticket.from_xml(self._unpack_resp_one(ret)) + def ticket_create(self, ticket, article, dynamic_fields=None, + attachments=None, **kwargs): + # ticket_create is deprecated, for backward compat, calls new method + return self.tc.TicketCreate(ticket, article, dynamic_fields=dynamic_fields, attachments=attachments, **kwargs) @authenticated - def ticket_search(self, dynamic_fields=None, **kwargs): - """ - @param dynamic_fields a list of Dynamic Fields, in addition to - the combination of `Name` and `Value`, also an `Operator` for the - comparison is expexted `Equals`, `Like`, `GreaterThan`, - `GreaterThanEquals`, `SmallerThan` or `SmallerThanEquals`. - The `Like` operator accepts a %-sign as wildcard. - @returns a list of matching TicketID - """ - df_search_list = [] - dynamic_field_requirements = ('Name', 'Value', 'Operator') - if not (dynamic_fields is None): - for df in dynamic_fields: - df.check_fields(dynamic_field_requirements) - if df.Operator == 'Equals': - df_search = DynamicField(Equals=df.Value) - elif df.Operator == 'Like': - df_search = DynamicField(Like=df.Value) - elif df.Operator == 'GreaterThan': - df_search = DynamicField(GreaterThan=df.Value) - elif df.Operator == 'GreaterThanEquals': - df_search = DynamicField(GreaterThanEquals=df.Value) - elif df.Operator == 'SmallerThan': - df_search = DynamicField(SmallerThan=df.Value) - elif df.Operator == 'SmallerThan': - df_search = DynamicField(SmallerThan=df.Value) - else: - raise WrongOperatorException() - df_search.XML_NAME = 'DynamicField_{0}'.format(df.Name) - df_search_list.append(df_search) - kwargs['DynamicFields'] = df_search_list - - ret = self.req('TicketSearch', **kwargs) - return [int(i.text) for i in self._unpack_resp_several(ret)] + def ticket_get(self, ticket_id, get_articles=False, get_dynamic_fields=False, get_attachments=False, *args, **kwargs): + # ticket_get is deprecated, for backward compat, calls new method + return self.tc.TicketGet(ticket_id, get_articles=get_articles, get_dynamic_fields=get_dynamic_fields, get_attachments=get_attachments, *args, **kwargs) @authenticated - def ticket_create(self, ticket, article, dynamic_fields=None, - attachments=None, **kwargs): - """ - @param ticket a Ticket - @param article an Article - @param dynamic_fields a list of Dynamic Fields - @param attachments a list of Attachments - @returns the ticketID, TicketNumber - """ - ticket_requirements = ( - ('StateID', 'State'), ('PriorityID', 'Priority'), - ('QueueID', 'Queue'), ) - article_requirements = ('Subject', 'Body', 'Charset', 'MimeType') - dynamic_field_requirements = ('Name', 'Value') - attachment_field_requirements = ('Content', 'ContentType', 'Filename') - ticket.check_fields(ticket_requirements) - article.check_fields(article_requirements) - if not (dynamic_fields is None): - for df in dynamic_fields: - df.check_fields(dynamic_field_requirements) - if not (attachments is None): - for att in attachments: - att.check_fields(attachment_field_requirements) - ret = self.req('TicketCreate', ticket=ticket, article=article, - dynamic_fields=dynamic_fields, - attachments=attachments, **kwargs) - elements = self._unpack_resp_several(ret) - infos = {extract_tagname(i): int(i.text) for i in elements} - return infos['TicketID'], infos['TicketNumber'] + def ticket_search(self, dynamic_fields=None, **kwargs): + # ticket_search is deprecated, for backward compat, calls new method + return self.tc.TicketSearch(dynamic_fields=dynamic_fields, **kwargs) @authenticated def ticket_update(self, ticket_id=None, ticket_number=None, ticket=None, article=None, dynamic_fields=None, attachments=None, **kwargs): - """ - @param ticket_id the ticket ID of the ticket to modify - @param ticket_number the ticket Number of the ticket to modify - @param ticket a ticket containing the fields to change on ticket - @param article a new Article to append to the ticket - @param dynamic_fields a list of Dynamic Fields to change on ticket - @param attachments a list of Attachments for a newly appended article - @returns the ticketID, TicketNumber - - - Mandatory : - `ticket_id` xor `ticket_number` - - `ticket` or `article` or `dynamic_fields` - - """ - if not (ticket_id is None): - kwargs['TicketID'] = ticket_id - elif not (ticket_number is None): - kwargs['TicketNumber'] = ticket_number - else: - raise ValueError('requires either ticket_id or ticket_number') - - if (ticket is None) and (article is None) and (dynamic_fields is None): - raise ValueError( - 'requires at least one among ticket, article, dynamic_fields') - elif (article is None) and not (attachments is None): - raise ValueError( - 'Attachments can only be created for a newly appended article') - else: - if (ticket): - kwargs['Ticket'] = ticket - if (article): - kwargs['Article'] = article - if (dynamic_fields): - kwargs['DynamicField'] = dynamic_fields - if (attachments): - kwargs['Attachment'] = attachments - - ret = self.req('TicketUpdate', **kwargs) - elements = self._unpack_resp_several(ret) - infos = {extract_tagname(i): int(i.text) for i in elements} - return infos['TicketID'], infos['TicketNumber'] + # ticket_update is deprecated, for backward compat, calls new method + return self.tc.TicketUpdate(ticket_id=ticket_id, ticket_number=ticket_number, + ticket=ticket, article=article, dynamic_fields=dynamic_fields, + attachments=attachments, **kwargs) + +def GenericTicketConnector(server, webservice_name='GenericTicketConnector', ssl_context=None): + """ DEPRECATED, ONLY HERE FOR BACKWARD COMPATIBILITY """ + from ticket.operations import SessionCreate,TicketCreate,TicketGet,TicketSearch,TicketUpdate + ticketconnector = WebService('GenericTicketConnector', 'http://www.otrs.org/TicketConnector', ssl_context=ssl_context, SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) + return OldGTCClass(server,tc=ticketconnector) diff --git a/otrs/objects.py b/otrs/objects.py index fc99d84..fca1749 100644 --- a/otrs/objects.py +++ b/otrs/objects.py @@ -3,8 +3,6 @@ import os import base64 - - class OTRSObject(object): """ Represents an object for OTRS (mappable to an XML element) """ @@ -104,7 +102,6 @@ def to_xml(self): root.append(e) return root - def extract_tagname(element): """ Returns the name of the tag, without namespace @@ -137,59 +134,22 @@ def autocast(s): except ValueError: return s +# the four functions below are here only for backward compatibility +# with old code that imported these classes from this file +# the classes are now in tickets/objects.py -class Attachment(OTRSObject): - XML_NAME = 'Attachment' - +def Ticket(*args, **kwargs): + import ticket.objects + return ticket.objects.Ticket(*args, **kwargs) -class DynamicField(OTRSObject): - XML_NAME = 'DynamicField' - - -class Article(OTRSObject): - XML_NAME = 'Article' - CHILD_MAP = {'Attachment': Attachment, 'DynamicField': DynamicField} - - def attachments(self): - try: - return self.childs['Attachment'] - except KeyError: - return [] +def Article(*args, **kwargs): + import ticket.objects + return ticket.objects.Article(*args, **kwargs) - def dynamicfields(self): - try: - return self.childs['DynamicField'] - except KeyError: - return [] - - def save_attachments(self, folder): - """ Saves the attachments of an article to the specified folder - - @param folder : a str, folder to save the attachments - """ - for a in self.attachments(): - fname = a.attrs['Filename'] - fpath = os.path.join(folder, fname) - content = a.attrs['Content'] - fcontent = base64.b64decode(content) - ffile = open(fpath, 'wb') - ffile.write(fcontent) - ffile.close() - - -class Ticket(OTRSObject): - XML_NAME = 'Ticket' - CHILD_MAP = {'Article': Article, 'DynamicField': DynamicField} - - def articles(self): - try: - return self.childs['Article'] - except KeyError: - return [] - - def dynamicfields(self): - try: - return self.childs['DynamicField'] - except KeyError: - return [] +def DynamicField(*args, **kwargs): + import ticket.objects + return ticket.objects.DynamicField(*args, **kwargs) +def Attachment(*args, **kwargs): + import ticket.objects + return ticket.objects.Attachment(*args, **kwargs) diff --git a/otrs/ticket/__init__.py b/otrs/ticket/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otrs/ticket/objects.py b/otrs/ticket/objects.py new file mode 100644 index 0000000..71794a2 --- /dev/null +++ b/otrs/ticket/objects.py @@ -0,0 +1,54 @@ +from ..objects import OTRSObject + +class Attachment(OTRSObject): + XML_NAME = 'Attachment' + +class DynamicField(OTRSObject): + XML_NAME = 'DynamicField' + +class Article(OTRSObject): + XML_NAME = 'Article' + CHILD_MAP = {'Attachment': Attachment, 'DynamicField': DynamicField} + + def attachments(self): + try: + return self.childs['Attachment'] + except KeyError: + return [] + + def dynamicfields(self): + try: + return self.childs['DynamicField'] + except KeyError: + return [] + + def save_attachments(self, folder): + """ Saves the attachments of an article to the specified folder + + @param folder : a str, folder to save the attachments + """ + for a in self.attachments(): + fname = a.attrs['Filename'] + fpath = os.path.join(folder, fname) + content = a.attrs['Content'] + fcontent = base64.b64decode(content) + ffile = open(fpath, 'wb') + ffile.write(fcontent) + ffile.close() + + +class Ticket(OTRSObject): + XML_NAME = 'Ticket' + CHILD_MAP = {'Article': Article, 'DynamicField': DynamicField} + + def articles(self): + try: + return self.childs['Article'] + except KeyError: + return [] + + def dynamicfields(self): + try: + return self.childs['DynamicField'] + except KeyError: + return [] diff --git a/otrs/ticket/operations.py b/otrs/ticket/operations.py new file mode 100644 index 0000000..20ee5ed --- /dev/null +++ b/otrs/ticket/operations.py @@ -0,0 +1,209 @@ +""" GenericTicketConnector Operations + +""" + +from .objects import Ticket as TicketObject +from ..client import OperationBase, authenticated +from ..objects import extract_tagname + +class Ticket(OperationBase): + + """ Base class for OTRS Ticket:: operations + + """ + +class Session(OperationBase): + """ Base class for OTRS Session:: operations + + """ + +class SessionCreate(Session): + """ Class to handle OTRS Session::SessionCreate operation + + """ + + def __call__(self, password, user_login=None, customer_user_login=None): + """ Logs the user or customeruser in + + @returns the session_id + """ + + if user_login: + ret = self.req('SessionCreate', + UserLogin=user_login, + Password=password) + else: + ret = self.req('SessionCreate', + CustomerUserLogin=customer_user_login, + Password=password) + signal = self._unpack_resp_one(ret) + session_id = signal.text + + # sets the session id for the entire client to this + self.session_id = session_id + + # returns the session id in case you want it, but its not normally needed + return session_id + +class TicketCreate(Ticket): + """ Class to handle OTRS Ticket::TicketCreate operation + + """ + + @authenticated + def __call__(self, ticket, article, dynamic_fields=None, + attachments=None, **kwargs): + """ + @param ticket a Ticket + @param article an Article + @param dynamic_fields a list of Dynamic Fields + @param attachments a list of Attachments + @returns the ticketID, TicketNumber + """ + ticket_requirements = ( + ('StateID', 'State'), ('PriorityID', 'Priority'), + ('QueueID', 'Queue'), ) + article_requirements = ('Subject', 'Body', 'Charset', 'MimeType') + dynamic_field_requirements = ('Name', 'Value') + attachment_field_requirements = ('Content', 'ContentType', 'Filename') + ticket.check_fields(ticket_requirements) + article.check_fields(article_requirements) + if not (dynamic_fields is None): + for df in dynamic_fields: + df.check_fields(dynamic_field_requirements) + if not (attachments is None): + for att in attachments: + att.check_fields(attachment_field_requirements) + ret = self.req('TicketCreate', ticket=ticket, article=article, + dynamic_fields=dynamic_fields, + attachments=attachments, **kwargs) + elements = self._unpack_resp_several(ret) + infos = {extract_tagname(i): int(i.text) for i in elements} + return infos['TicketID'], infos['TicketNumber'] + + +class TicketGet(Ticket): + """ Class to handle OTRS Ticket::TicketGet operation + + """ + + @authenticated + def __call__(self, ticket_id, get_articles=False, + get_dynamic_fields=False, + get_attachments=False, *args, **kwargs): + """ Get a ticket by id ; beware, TicketID != TicketNumber + + @param ticket_id : the TicketID of the ticket + @param get_articles : grab articles linked to the ticket + @param get_dynamic_fields : include dynamic fields in result + @param get_attachments : include attachments in result + + @return a `Ticket`, Ticket.articles() will give articles if relevant. + Ticket.articles()[i].attachments() will return the attachments for + an article, wheres Ticket.articles()[i].save_attachments() + will save the attachments of article[i] to the specified folder. + """ + params = {'TicketID': str(ticket_id)} + params.update(kwargs) + if get_articles: + params['AllArticles'] = True + if get_dynamic_fields: + params['DynamicFields'] = True + if get_attachments: + params['Attachments'] = True + + ret = self.req('TicketGet', **params) + return TicketObject.from_xml(self._unpack_resp_one(ret)) + + +class TicketSearch(Ticket): + """ Class to handle OTRS Ticket::TicketSearch operation + + """ + + @authenticated + def __call__(self, dynamic_fields=None, **kwargs): + """ + @param dynamic_fields a list of Dynamic Fields, in addition to + the combination of `Name` and `Value`, also an `Operator` for the + comparison is expexted `Equals`, `Like`, `GreaterThan`, + `GreaterThanEquals`, `SmallerThan` or `SmallerThanEquals`. + The `Like` operator accepts a %-sign as wildcard. + @returns a list of matching TicketID + """ + df_search_list = [] + dynamic_field_requirements = ('Name', 'Value', 'Operator') + if not (dynamic_fields is None): + for df in dynamic_fields: + df.check_fields(dynamic_field_requirements) + if df.Operator == 'Equals': + df_search = DynamicField(Equals=df.Value) + elif df.Operator == 'Like': + df_search = DynamicField(Like=df.Value) + elif df.Operator == 'GreaterThan': + df_search = DynamicField(GreaterThan=df.Value) + elif df.Operator == 'GreaterThanEquals': + df_search = DynamicField(GreaterThanEquals=df.Value) + elif df.Operator == 'SmallerThan': + df_search = DynamicField(SmallerThan=df.Value) + elif df.Operator == 'SmallerThan': + df_search = DynamicField(SmallerThan=df.Value) + else: + raise WrongOperatorException() + df_search.XML_NAME = 'DynamicField_{0}'.format(df.Name) + df_search_list.append(df_search) + kwargs['DynamicFields'] = df_search_list + + ret = self.req('TicketSearch', **kwargs) + return [int(i.text) for i in self._unpack_resp_several(ret)] + +class TicketUpdate(Ticket): + """ Class to handle OTRS Ticket::TicketUpdate operation + + """ + + @authenticated + def __call__(self, ticket_id=None, ticket_number=None, + ticket=None, article=None, dynamic_fields=None, + attachments=None, **kwargs): + """ + @param ticket_id the ticket ID of the ticket to modify + @param ticket_number the ticket Number of the ticket to modify + @param ticket a ticket containing the fields to change on ticket + @param article a new Article to append to the ticket + @param dynamic_fields a list of Dynamic Fields to change on ticket + @param attachments a list of Attachments for a newly appended article + @returns the ticketID, TicketNumber + + + Mandatory : - `ticket_id` xor `ticket_number` + - `ticket` or `article` or `dynamic_fields` + + """ + if not (ticket_id is None): + kwargs['TicketID'] = ticket_id + elif not (ticket_number is None): + kwargs['TicketNumber'] = ticket_number + else: + raise ValueError('requires either ticket_id or ticket_number') + + if (ticket is None) and (article is None) and (dynamic_fields is None): + raise ValueError( + 'requires at least one among ticket, article, dynamic_fields') + elif (article is None) and not (attachments is None): + raise ValueError( + 'Attachments can only be created for a newly appended article') + else: + if (ticket): + kwargs['Ticket'] = ticket + if (article): + kwargs['Article'] = article + if (dynamic_fields): + kwargs['DynamicField'] = dynamic_fields + if (attachments): + kwargs['Attachment'] = attachments + + ret = self.req('TicketUpdate', **kwargs) + elements = self._unpack_resp_several(ret) + infos = {extract_tagname(i): int(i.text) for i in elements} + return infos['TicketID'], infos['TicketNumber'] diff --git a/otrs/ticket/template.py b/otrs/ticket/template.py new file mode 100644 index 0000000..a5a16dc --- /dev/null +++ b/otrs/ticket/template.py @@ -0,0 +1,5 @@ +from .operations import SessionCreate,TicketCreate,TicketGet,TicketSearch,TicketUpdate +from ..client import WebService + +def GenericTicketConnectorSOAP(webservice_name='GenericTicketConnectorSOAP'): + return WebService(webservice_name, 'http://www.otrs.org/TicketConnector', SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) From 4084bad75a15f1311d06a8cdb1e102aabfa74f2c Mon Sep 17 00:00:00 2001 From: Michael Ducharme Date: Tue, 5 Jul 2016 21:37:24 -0500 Subject: [PATCH 2/9] spacing fix --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 32e70a3..9272909 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ Note: in older versions of OTRS, GenericTicketConnectorSOAP was called GenericTi server_uri = r'https://otrs.example.net' webservice_name = 'GenericTicketConnectorSOAP' - client = GenericInterfaceClient(server_uri, tc=GenericTicketConnectorSOAP(webservice_name)) + client = GenericInterfaceClient(server_uri, tc=GenericTicketConnectorSOAP(webservice_name)) Then authenticate, you have three choices : From 736ceb3e97c42b8741af10f95bc9c3f25e1febe3 Mon Sep 17 00:00:00 2001 From: Michael Ducharme Date: Tue, 5 Jul 2016 21:53:23 -0500 Subject: [PATCH 3/9] Fixed missing variable --- otrs/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otrs/client.py b/otrs/client.py index 4811e95..bbaae0f 100644 --- a/otrs/client.py +++ b/otrs/client.py @@ -320,5 +320,5 @@ def ticket_update(self, ticket_id=None, ticket_number=None, def GenericTicketConnector(server, webservice_name='GenericTicketConnector', ssl_context=None): """ DEPRECATED, ONLY HERE FOR BACKWARD COMPATIBILITY """ from ticket.operations import SessionCreate,TicketCreate,TicketGet,TicketSearch,TicketUpdate - ticketconnector = WebService('GenericTicketConnector', 'http://www.otrs.org/TicketConnector', ssl_context=ssl_context, SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) + ticketconnector = WebService(webservice_name, 'http://www.otrs.org/TicketConnector', ssl_context=ssl_context, SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) return OldGTCClass(server,tc=ticketconnector) From 38e7cd79ea55776354cb45ca037c3a701ca5b1a6 Mon Sep 17 00:00:00 2001 From: Michael Ducharme Date: Sun, 10 Jul 2016 20:28:04 -0500 Subject: [PATCH 4/9] Added full support for OTRS GenericFAQConnectorSOAP --- README.rst | 110 ++++++++++++++++++++++++++++++++++--- otrs/client.py | 3 +- otrs/faq/__init__.py | 0 otrs/faq/objects.py | 11 ++++ otrs/faq/operations.py | 87 +++++++++++++++++++++++++++++ otrs/faq/template.py | 5 ++ otrs/objects.py | 51 ++++++++++++++--- otrs/session/__init__.py | 0 otrs/session/operations.py | 39 +++++++++++++ otrs/ticket/objects.py | 45 +-------------- otrs/ticket/operations.py | 35 +----------- otrs/ticket/template.py | 3 +- 12 files changed, 296 insertions(+), 93 deletions(-) create mode 100644 otrs/faq/__init__.py create mode 100644 otrs/faq/objects.py create mode 100644 otrs/faq/operations.py create mode 100644 otrs/faq/template.py create mode 100644 otrs/session/__init__.py create mode 100644 otrs/session/operations.py diff --git a/README.rst b/README.rst index 9272909..417d5b2 100644 --- a/README.rst +++ b/README.rst @@ -6,13 +6,12 @@ Let you access the OTRS API a pythonic-way. Features -------- -- Implements fully communication with the ``GenericTicketConnector`` +- Implements fully communication with the ``GenericTicketConnectorSOAP`` and ``GenericFAQConnectorSOAP`` provided as webservice example by OTRS; - dynamic fields and attachments are supported; - authentication is handled programmatically, per-request or per-session; - calls are wrapped in OTRSClient methods; -- OTRS XML objects are mapped to Python-style objects see - objects.Article and objects.Ticket. +- OTRS XML objects are mapped to Python-style objects. To be done ---------- @@ -27,11 +26,12 @@ Install pip install python-otrs -Using ------ +Ticket and Session Operations +----------------------------- First make sure you installed the ``GenericTicketConnectorSOAP`` webservice, -see `official documentation`_. +see `official documentation`_. The file GenericTicketConnectorSOAP.yml can be downloaded +online as the basis for this service. Note: in older versions of OTRS, GenericTicketConnectorSOAP was called GenericTicketConnector @@ -93,7 +93,7 @@ Update an article : client.tc.TicketUpdate(t_id, t_upd) # appends a new article (attachments optional) - new_article = Article(Subject='Moar info', Body='blabla', Charset='UTF8', + new_article = Article(Subject='More info', Body='blabla', Charset='UTF8', MimeType='text/plain') client.tc.TicketUpdate(article=new_article, attachments=None) @@ -121,3 +121,99 @@ Many options are possible with requests, you can use all the options available in `official documentation`_. .. _official documentation: http://otrs.github.io/doc/manual/admin/4.0/en/html/genericinterface.html#generic-ticket-connector + +Public FAQ Operations +--------------------- + +First, make sure you have installed the open-source FAQ add-on module into your OTRS system and added the +GenericFAQConnectorSOAP web service by installing the GenericFAQConnector.yml file. + +:: + + from otrs.ticket.template import GenericTicketConnectorSOAP + from otrs.faq.template import GenericFAQConnectorSOAP + from otrs.client import GenericInterfaceClient + + client = GenericInterfaceClient('https://otrs.mycompany.com', tc=GenericTicketConnectorSOAP('GenericTicketConnectorSOAP'), fc=GenericFAQConnectorSOAP('GenericFAQConnectorSOAP')) + + # first, establish session with the TicketConnector + client.tc.SessionCreate(user_login='someotrsuser', password='p4ssw0rd') + +List FAQ Languages: + +:: + + langlist = client.fc.LanguageList() + for language in langlist: + print language.ID, language.Name + +List FAQ Categories that have Public FAQ items in them: + +:: + + catlist = client.fc.PublicCategoryList() + for category in catlist: + print category.ID, category.Name + +Retrieve a public FAQ article by ID +(note: FAQ Item ID is not the same as the item number!) + +:: + + # retrieves FAQ item ID #190 with attachment contents included + myfaqitem = client.fc.PublicFAQGet(190, get_attachments=True) + # print the FAQ's Problem field + print myfaqitem.Field2 + # saves attachments to folder ./tempattach + myfaqitem.save_attachments('./tempattach') + + +Custom Web Service Connectors +----------------------------- + +For the FAQ operations above, note that we still needed the Ticket connector to provide access +to the SessionCreate method. However, if your application only needs to work with FAQ articles +and not tickets, you may wish to create a custom web service in OTRS that not only includes +the four FAQ operations but also includes the SessionCreate operation to allow you to establish +a session. This is very easy to accommodate in python-otrs. + +First, in OTRS, do the following: + +1. In OTRS Admin->Web Services, add a new web service without using a .yml file. Name it something + like 'ImprovedFAQConnectorSOAP'. +2. In the settings for the web service, set the transport to HTTP::SOAP +3. Click Save +4. Click the 'Configure' button that has appeared next to HTTP::SOAP +5. Set the namespace name to whatever you want (ex. http://www.otrs.org/FAQConnector). +6. Enter the maximum message length you want (normally 10000000) +7. Save the changes and go back to the main web service configuration screen. +8. Add the operations you want to your custom webservice. For instance, for our improved FAQConnector, + you might add the four FAQ Operations and also the SessionCreate operation. +9. Save your webservice + +Now that we have a web service in OTRS, we can use our custom web service in python-otrs. To do this, +first create a 'template' for your new ImprovedFAQConnectorSOAP. Specify the namespace name assigned +in step 5 above as the second parameter to the WebService() call. + +:: + + from otrs.faq.operations import LanguageList,PublicCategoryList,PublicFAQGet,PublicFAQSearch + from otrs.session.operations import SessionCreate + from otrs.client import WebService + + def ImprovedFAQConnectorSOAP(webservice_name='ImprovedFAQConnectorSOAP'): + return WebService(webservice_name, 'http://www.otrs.org/FAQConnector', SessionCreate=SessionCreate(), LanguageList=LanguageList(),PublicCategoryList=PublicCategoryList(),PublicFAQGet=PublicFAQGet(),PublicFAQSearch=PublicFAQSearch()) + +Now, use your improved FAQ connector: + +:: + + from otrs.client import GenericInterfaceClient + + client = GenericInterfaceClient('https://otrs.mycompany.com', impfaqc=ImprovedFAQConnectorSOAP('ImprovedFAQConnectorSOAP')) + + # first, establish session + client.impfaqc.SessionCreate(user_login='someotrsuser', password='p4ssw0rd') + + # get an FAQ item: + client.impfaqc.PublicFAQGet(190) \ No newline at end of file diff --git a/otrs/client.py b/otrs/client.py index bbaae0f..d20706b 100644 --- a/otrs/client.py +++ b/otrs/client.py @@ -319,6 +319,7 @@ def ticket_update(self, ticket_id=None, ticket_number=None, def GenericTicketConnector(server, webservice_name='GenericTicketConnector', ssl_context=None): """ DEPRECATED, ONLY HERE FOR BACKWARD COMPATIBILITY """ - from ticket.operations import SessionCreate,TicketCreate,TicketGet,TicketSearch,TicketUpdate + from ticket.operations import TicketCreate,TicketGet,TicketSearch,TicketUpdate + from session.operations import SessionCreate ticketconnector = WebService(webservice_name, 'http://www.otrs.org/TicketConnector', ssl_context=ssl_context, SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) return OldGTCClass(server,tc=ticketconnector) diff --git a/otrs/faq/__init__.py b/otrs/faq/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otrs/faq/objects.py b/otrs/faq/objects.py new file mode 100644 index 0000000..a4b65db --- /dev/null +++ b/otrs/faq/objects.py @@ -0,0 +1,11 @@ +from ..objects import OTRSObject, Attachment, AttachmentContainer + +class Category(OTRSObject): + XML_NAME = 'Category' + +class Language(OTRSObject): + XML_NAME = 'Language' + +class FAQItem(OTRSObject, AttachmentContainer): + XML_NAME = 'FAQItem' + CHILD_MAP = {'Attachment': Attachment} diff --git a/otrs/faq/operations.py b/otrs/faq/operations.py new file mode 100644 index 0000000..07df4e3 --- /dev/null +++ b/otrs/faq/operations.py @@ -0,0 +1,87 @@ +""" FAQ:: Operations + +""" + +from .objects import Category as CategoryObject, Language as LanguageObject, FAQItem as FAQItemObject +from ..client import OperationBase, authenticated +from ..objects import extract_tagname + +class FAQ(OperationBase): + + """ Base class for OTRS FAQ:: operations + + """ + +class LanguageList(FAQ): + """ Class to handle OTRS ITSM FAQ::LanguageList operation + + """ + + @authenticated + def __call__(self, *args, **kwargs): + """ Returns the Language List from FAQ + + @returns list of languages + """ + + ret = self.req('LanguageList', **kwargs) + elements = self._unpack_resp_several(ret) + return [LanguageObject.from_xml(language) for language in elements] + + + +class PublicCategoryList(FAQ): + """ Class to handle OTRS ITSM FAQ::PublicCategoryList operation + + """ + + @authenticated + def __call__(self, **kwargs): + """ Returns the Public Category List from FAQ + + @returns list of category objects + """ + + ret = self.req('PublicCategoryList', **kwargs) + elements = self._unpack_resp_several(ret) + return [CategoryObject.from_xml(category) for category in elements] + + + +class PublicFAQGet(FAQ): + """ Class to handle OTRS ITSM FAQ::PublicFAQGet operation + + """ + + @authenticated + def __call__(self, item_id, get_attachments=False, **kwargs): + """ Get a public FAQItem by id + + @param item_id : the ItemID of the public FAQItem + NOTE: ItemID != FAQ Number + + @return an `FAQItem` + """ + params = {'ItemID': str(item_id)} + params.update(kwargs) + if get_attachments: + params['GetAttachmentContents'] = 1 + else: + params['GetAttachmentContents'] = 0 + + ret = self.req('PublicFAQGet', **params) + return FAQItemObject.from_xml(self._unpack_resp_one(ret)) + +class PublicFAQSearch(FAQ): + """ Class to handle OTRS ITSM FAQ::PublicFAQSearch operation + + """ + + @authenticated + def __call__(self, *args, **kwargs): + """ + @returns a list of matching public FAQItem IDs + """ + + ret = self.req('PublicFAQSearch', **kwargs) + return [int(i.text) for i in self._unpack_resp_several(ret)] diff --git a/otrs/faq/template.py b/otrs/faq/template.py new file mode 100644 index 0000000..47f49ce --- /dev/null +++ b/otrs/faq/template.py @@ -0,0 +1,5 @@ +from .operations import LanguageList,PublicCategoryList,PublicFAQGet,PublicFAQSearch +from ..client import WebService + +def GenericFAQConnectorSOAP(webservice_name='GenericFAQConnectorSOAP'): + return WebService(webservice_name, 'http://www.otrs.org/FAQConnector', LanguageList=LanguageList(),PublicCategoryList=PublicCategoryList(),PublicFAQGet=PublicFAQGet(),PublicFAQSearch=PublicFAQSearch()) diff --git a/otrs/objects.py b/otrs/objects.py index fca1749..a563f48 100644 --- a/otrs/objects.py +++ b/otrs/objects.py @@ -134,7 +134,49 @@ def autocast(s): except ValueError: return s -# the four functions below are here only for backward compatibility +class Attachment(OTRSObject): + XML_NAME = 'Attachment' + +class DynamicField(OTRSObject): + XML_NAME = 'DynamicField' + +class AttachmentContainer(object): + """ For objects that can have attachments in them (ex. ticket articles, faq + items) they should inherit this class in addition to OTRSObject + """ + + def attachments(self): + try: + return self.childs['Attachment'] + except KeyError: + return [] + + def save_attachments(self, folder): + """ Saves the attachments of an article to the specified folder + + @param folder : a str, folder to save the attachments + """ + for a in self.attachments(): + fname = a.attrs['Filename'] + fpath = os.path.join(folder, fname) + content = a.attrs['Content'] + fcontent = base64.b64decode(content) + ffile = open(fpath, 'wb') + ffile.write(fcontent) + ffile.close() + +class DynamicFieldContainer(object): + """ For objects that can have dynamic fields in them (ex. tickets, articles) they should inherit this class in addition to OTRSObject + """ + + def dynamicfields(self): + try: + return self.childs['DynamicField'] + except KeyError: + return [] + + +# the two functions below are here only for backward compatibility # with old code that imported these classes from this file # the classes are now in tickets/objects.py @@ -146,10 +188,3 @@ def Article(*args, **kwargs): import ticket.objects return ticket.objects.Article(*args, **kwargs) -def DynamicField(*args, **kwargs): - import ticket.objects - return ticket.objects.DynamicField(*args, **kwargs) - -def Attachment(*args, **kwargs): - import ticket.objects - return ticket.objects.Attachment(*args, **kwargs) diff --git a/otrs/session/__init__.py b/otrs/session/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otrs/session/operations.py b/otrs/session/operations.py new file mode 100644 index 0000000..91d72b9 --- /dev/null +++ b/otrs/session/operations.py @@ -0,0 +1,39 @@ +""" Session:: operations + +""" + +from ..client import OperationBase, authenticated +from ..objects import extract_tagname + +class Session(OperationBase): + """ Base class for OTRS Session:: operations + + """ + +class SessionCreate(Session): + """ Class to handle OTRS Session::SessionCreate operation + + """ + + def __call__(self, password, user_login=None, customer_user_login=None): + """ Logs the user or customeruser in + + @returns the session_id + """ + + if user_login: + ret = self.req('SessionCreate', + UserLogin=user_login, + Password=password) + else: + ret = self.req('SessionCreate', + CustomerUserLogin=customer_user_login, + Password=password) + signal = self._unpack_resp_one(ret) + session_id = signal.text + + # sets the session id for the entire client to this + self.session_id = session_id + + # returns the session id in case you want it, but its not normally needed + return session_id diff --git a/otrs/ticket/objects.py b/otrs/ticket/objects.py index 71794a2..8663866 100644 --- a/otrs/ticket/objects.py +++ b/otrs/ticket/objects.py @@ -1,43 +1,10 @@ -from ..objects import OTRSObject +from ..objects import OTRSObject, Attachment, DynamicField, AttachmentContainer, DynamicFieldContainer -class Attachment(OTRSObject): - XML_NAME = 'Attachment' - -class DynamicField(OTRSObject): - XML_NAME = 'DynamicField' - -class Article(OTRSObject): +class Article(OTRSObject, AttachmentContainer, DynamicFieldContainer): XML_NAME = 'Article' CHILD_MAP = {'Attachment': Attachment, 'DynamicField': DynamicField} - def attachments(self): - try: - return self.childs['Attachment'] - except KeyError: - return [] - - def dynamicfields(self): - try: - return self.childs['DynamicField'] - except KeyError: - return [] - - def save_attachments(self, folder): - """ Saves the attachments of an article to the specified folder - - @param folder : a str, folder to save the attachments - """ - for a in self.attachments(): - fname = a.attrs['Filename'] - fpath = os.path.join(folder, fname) - content = a.attrs['Content'] - fcontent = base64.b64decode(content) - ffile = open(fpath, 'wb') - ffile.write(fcontent) - ffile.close() - - -class Ticket(OTRSObject): +class Ticket(OTRSObject, DynamicFieldContainer): XML_NAME = 'Ticket' CHILD_MAP = {'Article': Article, 'DynamicField': DynamicField} @@ -46,9 +13,3 @@ def articles(self): return self.childs['Article'] except KeyError: return [] - - def dynamicfields(self): - try: - return self.childs['DynamicField'] - except KeyError: - return [] diff --git a/otrs/ticket/operations.py b/otrs/ticket/operations.py index 20ee5ed..cc2d4b6 100644 --- a/otrs/ticket/operations.py +++ b/otrs/ticket/operations.py @@ -1,4 +1,4 @@ -""" GenericTicketConnector Operations +""" Ticket:: Operations """ @@ -12,39 +12,6 @@ class Ticket(OperationBase): """ -class Session(OperationBase): - """ Base class for OTRS Session:: operations - - """ - -class SessionCreate(Session): - """ Class to handle OTRS Session::SessionCreate operation - - """ - - def __call__(self, password, user_login=None, customer_user_login=None): - """ Logs the user or customeruser in - - @returns the session_id - """ - - if user_login: - ret = self.req('SessionCreate', - UserLogin=user_login, - Password=password) - else: - ret = self.req('SessionCreate', - CustomerUserLogin=customer_user_login, - Password=password) - signal = self._unpack_resp_one(ret) - session_id = signal.text - - # sets the session id for the entire client to this - self.session_id = session_id - - # returns the session id in case you want it, but its not normally needed - return session_id - class TicketCreate(Ticket): """ Class to handle OTRS Ticket::TicketCreate operation diff --git a/otrs/ticket/template.py b/otrs/ticket/template.py index a5a16dc..6ab9267 100644 --- a/otrs/ticket/template.py +++ b/otrs/ticket/template.py @@ -1,4 +1,5 @@ -from .operations import SessionCreate,TicketCreate,TicketGet,TicketSearch,TicketUpdate +from .operations import TicketCreate,TicketGet,TicketSearch,TicketUpdate +from ..session.operations import SessionCreate from ..client import WebService def GenericTicketConnectorSOAP(webservice_name='GenericTicketConnectorSOAP'): From f7a573387b4dbd7207c27493e63401a606d162b2 Mon Sep 17 00:00:00 2001 From: Michael Ducharme Date: Sun, 10 Jul 2016 20:44:59 -0500 Subject: [PATCH 5/9] Corrections in README --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 417d5b2..808dcbb 100644 --- a/README.rst +++ b/README.rst @@ -166,6 +166,15 @@ Retrieve a public FAQ article by ID print myfaqitem.Field2 # saves attachments to folder ./tempattach myfaqitem.save_attachments('./tempattach') + +Search for an FAQ article + +:: + + #find all FAQ articles with Windows in title: + results = client.fc.PublicFAQSearch(Title='*Windows*') + for faqitemid in results: + print "Found FAQ item ID containing Windows: " + str(faqitemid) Custom Web Service Connectors @@ -198,7 +207,7 @@ in step 5 above as the second parameter to the WebService() call. :: from otrs.faq.operations import LanguageList,PublicCategoryList,PublicFAQGet,PublicFAQSearch - from otrs.session.operations import SessionCreate + from otrs.session.operations import SessionCreate from otrs.client import WebService def ImprovedFAQConnectorSOAP(webservice_name='ImprovedFAQConnectorSOAP'): From ae7efab2fb095b6b2dc68cdadf94a21c3a124534 Mon Sep 17 00:00:00 2001 From: EWSterrenburg Date: Thu, 14 Jul 2016 10:24:29 +0200 Subject: [PATCH 6/9] Minor changes in README --- README.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 808dcbb..7ee6abf 100644 --- a/README.rst +++ b/README.rst @@ -8,16 +8,16 @@ Features - Implements fully communication with the ``GenericTicketConnectorSOAP`` and ``GenericFAQConnectorSOAP`` provided as webservice example by OTRS; -- dynamic fields and attachments are supported; -- authentication is handled programmatically, per-request or per-session; -- calls are wrapped in OTRSClient methods; +- Dynamic fields and attachments are supported; +- Authentication is handled programmatically, per-request or per-session; +- Calls are wrapped in OTRSClient methods; - OTRS XML objects are mapped to Python-style objects. To be done ---------- - Test for python3 compatibility and make resulting changes; -- improve and extend ``tests.py``; +- Improve and extend ``tests.py``. Install ------- @@ -56,7 +56,7 @@ Then authenticate, you have three choices : client.tc.SessionCreate(customer_user_login='login' , password='password') # save user in memory - client.register_credentials(user='login', 'password') + client.register_credentials(user='login', password='password') Play ! @@ -155,7 +155,7 @@ List FAQ Categories that have Public FAQ items in them: for category in catlist: print category.ID, category.Name -Retrieve a public FAQ article by ID +Retrieve a pubblic FAQ article by ID (note: FAQ Item ID is not the same as the item number!) :: From 96ecfa6477773167b6dee8764f7004c470ef8fee Mon Sep 17 00:00:00 2001 From: EWSterrenburg Date: Thu, 14 Jul 2016 14:06:59 +0200 Subject: [PATCH 7/9] Linting + some minor changes (mostly on imports) --- otrs/client.py | 217 ++++++++++++++++++++++++------------- otrs/faq/objects.py | 12 +- otrs/faq/operations.py | 46 +++----- otrs/faq/template.py | 16 ++- otrs/objects.py | 71 ++++++++---- otrs/session/operations.py | 20 ++-- otrs/ticket/objects.py | 14 ++- otrs/ticket/operations.py | 52 ++++----- otrs/ticket/template.py | 19 +++- 9 files changed, 288 insertions(+), 179 deletions(-) diff --git a/otrs/client.py b/otrs/client.py index d20706b..93ca8dc 100644 --- a/otrs/client.py +++ b/otrs/client.py @@ -1,46 +1,64 @@ +"""OTRS :: client.""" try: import urllib.request as urllib2 except ImportError: import urllib2 from posixpath import join as urljoin import xml.etree.ElementTree as etree -from .objects import OTRSObject, extract_tagname +from otrs.objects import OTRSObject, extract_tagname import codecs import sys import abc + class OTRSError(Exception): + """Base class for OTRS Errors.""" + def __init__(self, fd): + """Initialize OTRS Error.""" self.code = fd.getcode() self.msg = fd.read() def __str__(self): + """Return error message for OTRS Error.""" return '{} : {}'.format(self.code, self.msg) class SOAPError(OTRSError): + """OTRS Error originating from an incorrect SOAP request.""" + def __init__(self, tag): + """Initialize OTRS SOAPError.""" d = {extract_tagname(i): i.text for i in tag.getchildren()} self.errcode = d['ErrorCode'] self.errmsg = d['ErrorMessage'] def __str__(self): + """Return error message for OTRS SOAPError.""" return '{} ({})'.format(self.errmsg, self.errcode) class NoCredentialsException(OTRSError): + """OTRS Error that is returned when no credentials are provided.""" + def __init__(self): + """Initialize OTRS NoCredentialsException.""" pass def __str__(self): + """Return error message for OTRS NoCredentialsException.""" return 'Register credentials first with register_credentials() method' class WrongOperatorException(OTRSError): + """OTRS Error that is returned when a non-existent operation is called.""" + def __init__(self): + """Initialize OTRS WrongOperatorException.""" pass def __str__(self): + """Return error message for OTRS WrongOperatorException.""" return '''Please use one of the following operators for the query on a dynamic field: `Equals`, `Like`, `GreaterThan`, `GreaterThanEquals`, `SmallerThan` or `SmallerThanEquals`. @@ -48,9 +66,7 @@ def __str__(self): def authenticated(func): - """ Decorator to add authentication parameters to a request - """ - + """Decorator to add authentication parameters to a request.""" def add_auth(self, *args, **kwargs): if self.session_id: kwargs['SessionID'] = self.session_id @@ -64,59 +80,75 @@ def add_auth(self, *args, **kwargs): return add_auth + class OperationBase(object): - """ Base class for OTRS operations + """Base class for OTRS operations.""" - """ __metaclass__ = abc.ABCMeta - def __init__(self, opName): - self.operName = opName # otrs connector operation name - self.wsObject = None # web services object this operation belongs to - - def __init__(self): - self.operName = type(self).__name__ # otrs connector operation name - self.wsObject = None # web services object this operation belongs to + def __init__(self, opName=None): + """Initialize OperationBase.""" + if opName is None: + self.operName = type(self).__name__ + else: + self.operName = opName # otrs connector operation name + self.wsObject = None # web services object this operation belongs to def getWebServiceObjectAttribute(self, attribName): + """Return attribute of the WebService object.""" return getattr(self.wsObject, attribName) def getClientObjectAttribute(self, attribName): + """Return attribute of the clientobject of the WebService object.""" return self.wsObject.getClientObjectAttribute(attribName) def setClientObjectAttribute(self, attribName, attribValue): + """Set attribute of the clientobject of the WebService object.""" self.wsObject.setClientObjectAttribute(attribName, attribValue) @abc.abstractmethod def __call__(self): + """.""" return @property def endpoint(self): + """Return endpoint of WebService object.""" return self.getWebServiceObjectAttribute('endpoint') @property def login(self): + """Get login attribute of the clientobject of the WebService object.""" return self.getClientObjectAttribute('login') @property def password(self): + """Return password attribute of the clientobject of the WebService.""" return self.getClientObjectAttribute('password') @property def session_id(self): + """Return session_id of WebService object.""" return self.getClientObjectAttribute('session_id') @session_id.setter def session_id(self, sessionid): + """Set session_id of WebService object.""" self.setClientObjectAttribute('session_id', sessionid) @property def soap_envelope(self): - return '{}' + """Return session_id of WebService object.""" + soap_envelope = '{}' + \ + '' + return soap_envelope def req(self, reqname, *args, **kwargs): - """ Wrapper arround a SOAP request + """Wrapper arround a SOAP request. + @param reqname: the SOAP name of the request @param kwargs : to define the tags included in the request. @return : the full etree.Element of the response @@ -127,7 +159,6 @@ def req(self, reqname, *args, **kwargs): - list of `OTRSObject`s: each `OTRSObject`s in the list will be serialized with their `.to_xml()` (used for dynamic fields and attachments). - """ xml_req_root = etree.Element(reqname) @@ -147,7 +178,8 @@ def req(self, reqname, *args, **kwargs): self.endpoint, self._pack_req(xml_req_root), {'Content-Type': 'text/xml;charset=utf-8'}) - if (sys.version_info[0] == 3 and sys.version_info < (3,4,3)) or sys.version_info < (2,7,9): + if ((sys.version_info[0] == 3 and sys.version_info < (3, 4, 3)) or + (sys.version_info < (2, 7, 9))): fd = urllib2.urlopen(request) else: fd = urllib2.urlopen(request, context=self.ssl_context) @@ -172,7 +204,8 @@ def req(self, reqname, *args, **kwargs): @staticmethod def _unpack_resp_several(element): - """ + """Unpack an etree element and return a list of children. + @param element : a etree.Element @return : a list of etree.Element """ @@ -180,66 +213,71 @@ def _unpack_resp_several(element): @staticmethod def _unpack_resp_one(element): - """ + """Unpack an etree element an return first child. + @param element : a etree.Element @return : a etree.Element (first child of the response) """ return element.getchildren()[0].getchildren()[0].getchildren()[0] def _pack_req(self, element): - """ + """Pack an etree Element. + @param element : a etree.Element @returns : a string, wrapping element within the request tags - """ - return self.soap_envelope.format(codecs.decode(etree.tostring(element),'utf-8')).encode('utf-8') + return self.soap_envelope.format( + codecs.decode(etree.tostring(element), 'utf-8')).encode('utf-8') -class WebService(object): - """ Base class for OTRS Web Service - """ +class WebService(object): + """Base class for OTRS Web Service.""" def __init__(self, wsName, wsNamespace, **kwargs): - self.clientObject = None # link to parent client object - self.wsName = wsName # name for OTRS web service - self.wsNamespace = wsNamespace # OTRS namespace url + """Initialize WebService object.""" + self.clientObject = None # link to parent client object + self.wsName = wsName # name for OTRS web service + self.wsNamespace = wsNamespace # OTRS namespace url # add all variables in kwargs into the local dictionary self.__dict__.update(kwargs) # for operations, set backlinks to their associated webservice for arg in kwargs: + # if attribute is type OperationBase, set backlink to WebService if isinstance(getattr(self, arg), OperationBase): getattr(self, arg).wsObject = self - # if attribute is type OperationBase, set backlink to WebService # set defaults if attributes are not present if not hasattr(self, 'wsRequestNameScheme'): self.wsRequestNameScheme = 'DATA' if not hasattr(self, 'wsResponseNameScheme'): - self.wsResponseNameScheme = 'DATA' + ns = 'DATA' + self.wsResponseNameScheme = ns def getClientObjectAttribute(self, attribName): - return getattr(self.clientObject,attribName) + """Return attribute of the clientobject of the WebService object.""" + return getattr(self.clientObject, attribName) def setClientObjectAttribute(self, attribName, attribValue): - setattr(self.clientObject,attribName,attribValue) + """Set attribute of the clientobject of the WebService object.""" + setattr(self.clientObject, attribName, attribValue) @property def endpoint(self): - return urljoin(self.getClientObjectAttribute('giurl'),self.wsName) + """Return endpoint of WebService object.""" + return urljoin(self.getClientObjectAttribute('giurl'), self.wsName) class GenericInterfaceClient(object): - """ Client for the OTRS Generic Interface - - """ + """Client for the OTRS Generic Interface.""" def __init__(self, server, **kwargs): - """ @param server : the http(s) URL of the root installation of OTRS - (e.g: https://tickets.example.net) - """ + """Initialize GenericInterfaceClient. + @param server : the http(s) URL of the root installation of OTRS + (e.g: https://tickets.example.net) + """ # add all variables in kwargs into the local dictionary self.__dict__.update(kwargs) @@ -247,8 +285,9 @@ def __init__(self, server, **kwargs): # to this client object to allow access to session login/password for arg in kwargs: - if isinstance(getattr(self, arg), WebService): - getattr(self, arg).clientObject = self # set backlink for web services to this obj + # set backlink for web services to this obj + if isinstance(getattr(self, arg), WebService): + getattr(self, arg).clientObject = self self.login = None self.password = None self.session_id = None @@ -256,70 +295,92 @@ def __init__(self, server, **kwargs): server, 'otrs/nph-genericinterface.pl/Webservice/') def register_credentials(self, login, password): - """ Save the identifiers in memory, they will be used with each - subsequent request requiring authentication + """Save the identifiers in memory. + + They will be used with each subsequent request requiring authentication """ self.login = login self.password = password + class OldGTCClass(GenericInterfaceClient): - """ Old deprecated generic ticket connector class, used for - backward compatibility with previous versions. All - methods in here are deprecated + """DEPRECATED - Old generic ticket connector class. + + Used for backward compatibility with previous versions. All + methods in here are deprecated. """ def session_create(self, password, user_login=None, - customer_user_login=None): - """ DEPRECATED - Logs the user or customeruser in + customer_user_login=None): + """DEPRECATED - creates a session for an User or CustomerUser. @returns the session_id """ - self.tc.SessionCreate(password,user_login=user_login,customer_user_login=customer_user_login) + self.tc.SessionCreate(password, user_login=user_login, + customer_user_login=customer_user_login) def user_session_register(self, user, password): - """ Logs the user in and stores the session_id for subsequent requests - DEPRECATED - """ - self.session_create( - password=password, - user_login=user) + """DEPRECATED - creates a session for an User.""" + self.session_create(password=password, user_login=user) def customer_user_session_register(self, user, password): - """ Logs the customer_user in and stores the session_id for subsequent - requests. DEPRECATED - """ - self.session_create( - password=password, - customer_user_login=user) + """DEPRECATED - creates a session for a CustomerUser.""" + self.session_create(password=password, customer_user_login=user) @authenticated def ticket_create(self, ticket, article, dynamic_fields=None, attachments=None, **kwargs): - # ticket_create is deprecated, for backward compat, calls new method - return self.tc.TicketCreate(ticket, article, dynamic_fields=dynamic_fields, attachments=attachments, **kwargs) + """DEPRECATED - now calls operation of GenericTicketConnectorSOAP.""" + return self.tc.TicketCreate(ticket, + article, + dynamic_fields=dynamic_fields, + attachments=attachments, + **kwargs) @authenticated - def ticket_get(self, ticket_id, get_articles=False, get_dynamic_fields=False, get_attachments=False, *args, **kwargs): - # ticket_get is deprecated, for backward compat, calls new method - return self.tc.TicketGet(ticket_id, get_articles=get_articles, get_dynamic_fields=get_dynamic_fields, get_attachments=get_attachments, *args, **kwargs) + def ticket_get(self, ticket_id, get_articles=False, + get_dynamic_fields=False, get_attachments=False, + *args, **kwargs): + """DEPRECATED - now calls operation of GenericTicketConnectorSOAP.""" + return self.tc.TicketGet(ticket_id, + get_articles=get_articles, + get_dynamic_fields=get_dynamic_fields, + get_attachments=get_attachments, + *args, + **kwargs) @authenticated def ticket_search(self, dynamic_fields=None, **kwargs): - # ticket_search is deprecated, for backward compat, calls new method + """DEPRECATED - now calls operation of GenericTicketConnectorSOAP.""" return self.tc.TicketSearch(dynamic_fields=dynamic_fields, **kwargs) @authenticated def ticket_update(self, ticket_id=None, ticket_number=None, ticket=None, article=None, dynamic_fields=None, attachments=None, **kwargs): - # ticket_update is deprecated, for backward compat, calls new method - return self.tc.TicketUpdate(ticket_id=ticket_id, ticket_number=ticket_number, - ticket=ticket, article=article, dynamic_fields=dynamic_fields, - attachments=attachments, **kwargs) - -def GenericTicketConnector(server, webservice_name='GenericTicketConnector', ssl_context=None): - """ DEPRECATED, ONLY HERE FOR BACKWARD COMPATIBILITY """ - from ticket.operations import TicketCreate,TicketGet,TicketSearch,TicketUpdate - from session.operations import SessionCreate - ticketconnector = WebService(webservice_name, 'http://www.otrs.org/TicketConnector', ssl_context=ssl_context, SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) - return OldGTCClass(server,tc=ticketconnector) + """DEPRECATED - now calls operation of GenericTicketConnectorSOAP.""" + return self.tc.TicketUpdate(ticket_id=ticket_id, + ticket_number=ticket_number, + ticket=ticket, + article=article, + dynamic_fields=dynamic_fields, + attachments=attachments, **kwargs) + + +def GenericTicketConnector(server, + webservice_name='GenericTicketConnector', + ssl_context=None): + """DEPRECATED - now calls operation of GenericTicketConnectorSOAP.""" + from otrs.ticket.operations import TicketCreate, TicketGet + from otrs.ticket.operations import TicketSearch, TicketUpdate + from otrs.session.operations import SessionCreate + ticketconnector = WebService( + webservice_name, + 'http://www.otrs.org/TicketConnector', + ssl_context=ssl_context, + SessionCreate=SessionCreate(), + TicketCreate=TicketCreate(), + TicketGet=TicketGet(), + TicketSearch=TicketSearch(), + TicketUpdate=TicketUpdate()) + return OldGTCClass(server, tc=ticketconnector) diff --git a/otrs/faq/objects.py b/otrs/faq/objects.py index a4b65db..df47555 100644 --- a/otrs/faq/objects.py +++ b/otrs/faq/objects.py @@ -1,11 +1,21 @@ -from ..objects import OTRSObject, Attachment, AttachmentContainer +"""OTRS :: faq :: objects.""" +from otrs.objects import OTRSObject, Attachment, AttachmentContainer + class Category(OTRSObject): + """An OTRS FAQ Category.""" + XML_NAME = 'Category' + class Language(OTRSObject): + """An OTRS FAQ Language.""" + XML_NAME = 'Language' + class FAQItem(OTRSObject, AttachmentContainer): + """An OTRS FAQ Item.""" + XML_NAME = 'FAQItem' CHILD_MAP = {'Attachment': Attachment} diff --git a/otrs/faq/operations.py b/otrs/faq/operations.py index 07df4e3..c67ba05 100644 --- a/otrs/faq/operations.py +++ b/otrs/faq/operations.py @@ -1,61 +1,48 @@ -""" FAQ:: Operations +"""OTRS :: faq :: operations.""" +from otrs.faq.objects import Category as CategoryObject +from otrs.faq.objects import Language as LanguageObject +from otrs.faq.objects import FAQItem as FAQItemObject +from otrs.client import OperationBase, authenticated -""" - -from .objects import Category as CategoryObject, Language as LanguageObject, FAQItem as FAQItemObject -from ..client import OperationBase, authenticated -from ..objects import extract_tagname class FAQ(OperationBase): + """Base class for OTRS FAQ:: operations.""" - """ Base class for OTRS FAQ:: operations - - """ class LanguageList(FAQ): - """ Class to handle OTRS ITSM FAQ::LanguageList operation - - """ + """Class to handle OTRS ITSM FAQ::LanguageList operation.""" @authenticated def __call__(self, *args, **kwargs): - """ Returns the Language List from FAQ + """Return the Language List from FAQ. @returns list of languages """ - ret = self.req('LanguageList', **kwargs) elements = self._unpack_resp_several(ret) return [LanguageObject.from_xml(language) for language in elements] - class PublicCategoryList(FAQ): - """ Class to handle OTRS ITSM FAQ::PublicCategoryList operation - - """ + """Class to handle OTRS ITSM FAQ::PublicCategoryList operation.""" @authenticated def __call__(self, **kwargs): - """ Returns the Public Category List from FAQ + """Return the Public Category List from FAQ. @returns list of category objects """ - ret = self.req('PublicCategoryList', **kwargs) elements = self._unpack_resp_several(ret) return [CategoryObject.from_xml(category) for category in elements] - class PublicFAQGet(FAQ): - """ Class to handle OTRS ITSM FAQ::PublicFAQGet operation - - """ + """Class to handle OTRS ITSM FAQ::PublicFAQGet operation.""" @authenticated def __call__(self, item_id, get_attachments=False, **kwargs): - """ Get a public FAQItem by id + """Get a public FAQItem by id. @param item_id : the ItemID of the public FAQItem NOTE: ItemID != FAQ Number @@ -72,16 +59,15 @@ def __call__(self, item_id, get_attachments=False, **kwargs): ret = self.req('PublicFAQGet', **params) return FAQItemObject.from_xml(self._unpack_resp_one(ret)) -class PublicFAQSearch(FAQ): - """ Class to handle OTRS ITSM FAQ::PublicFAQSearch operation - """ +class PublicFAQSearch(FAQ): + """Class to handle OTRS ITSM FAQ :: PublicFAQSearch operation.""" @authenticated def __call__(self, *args, **kwargs): - """ + """Search for matching public FAQItems. + @returns a list of matching public FAQItem IDs """ - ret = self.req('PublicFAQSearch', **kwargs) return [int(i.text) for i in self._unpack_resp_several(ret)] diff --git a/otrs/faq/template.py b/otrs/faq/template.py index 47f49ce..d7cf5ed 100644 --- a/otrs/faq/template.py +++ b/otrs/faq/template.py @@ -1,5 +1,15 @@ -from .operations import LanguageList,PublicCategoryList,PublicFAQGet,PublicFAQSearch -from ..client import WebService +"""OTRS :: faq :: template.""" +from otrs.faq.operations import LanguageList, PublicCategoryList +from otrs.faq.operations import PublicFAQGet, PublicFAQSearch +from otrs.client import WebService def GenericFAQConnectorSOAP(webservice_name='GenericFAQConnectorSOAP'): - return WebService(webservice_name, 'http://www.otrs.org/FAQConnector', LanguageList=LanguageList(),PublicCategoryList=PublicCategoryList(),PublicFAQGet=PublicFAQGet(),PublicFAQSearch=PublicFAQSearch()) + """Return a GenericFAQConnectorSOAP Webservice object. + + @returns a WebService object with the GenericFAQConnectorSOAP operations + """ + return WebService(webservice_name, 'http://www.otrs.org/FAQConnector', + LanguageList=LanguageList(), + PublicCategoryList=PublicCategoryList(), + PublicFAQGet=PublicFAQGet(), + PublicFAQSearch=PublicFAQSearch()) diff --git a/otrs/objects.py b/otrs/objects.py index a563f48..9bc9796 100644 --- a/otrs/objects.py +++ b/otrs/objects.py @@ -1,22 +1,26 @@ +"""OTRS :: objects.""" from __future__ import unicode_literals import xml.etree.ElementTree as etree import os +import sys import base64 + class OTRSObject(object): - """ Represents an object for OTRS (mappable to an XML element) - """ + """Represents an object for OTRS (mappable to an XML element).""" # Map : {'TagName' -> Class} - CHILD_MAP = {} def __init__(self, *args, **kwargs): + """Initialize OTRS Object.""" self.attrs = kwargs self.childs = {} def __getattr__(self, k): - """ attrs are simple xml child tags (val), complex children, + """Get an attribute for aan OTRSObject. + + attrs are simple xml child tags (val), complex children, are accessible via dedicated methods. @returns a simple type @@ -25,7 +29,8 @@ def __getattr__(self, k): @classmethod def from_xml(cls, xml_element): - """ + """Create an OTRS Object from xml. + @param xml_element an etree.Element @returns an OTRSObject """ @@ -55,7 +60,8 @@ def from_xml(cls, xml_element): return obj def add_child(self, childobj): - """ + """Add a child object to an OTRS Object. + @param childobj : an OTRSObject """ xml_name = childobj.XML_NAME @@ -66,7 +72,8 @@ def add_child(self, childobj): self.childs[xml_name] = [childobj] def check_fields(self, fields): - """ Checks that the list of fields is bound + """Check that the list of fields is bound. + @param fields rules, as list items n fields can be either : @@ -90,20 +97,25 @@ def check_fields(self, fields): raise ValueError('{} should be filled'.format(i)) def to_xml(self): - """ + """Create an XML representation of an OTRS Object. + @returns am etree.Element """ root = etree.Element(self.XML_NAME) for k, v in self.attrs.items(): e = etree.Element(k) - if isinstance(e, str): # True + if isinstance(e, str): v = v.encode('utf-8') - e.text = unicode(v) + if sys.version_info[0] == 3: + e.text = str(v) + else: + e.text = unicode(v) root.append(e) return root + def extract_tagname(element): - """ Returns the name of the tag, without namespace + """Return the name of the tag, without namespace. element.tag lib gives "{namespace}tagname", we want only "tagname" @@ -116,16 +128,15 @@ def extract_tagname(element): except IndexError: # if it's not namespaced, then return the tag name itself return element.tag - #raise ValueError('"{}" is not a tag name'.format(qualified_name)) + # raise ValueError('"{}" is not a tag name'.format(qualified_name)) def autocast(s): - """ Tries to guess the simple type and convert the value to it. + """Try to guess the simple type and convert the value to it. @param s string @returns the relevant type : a float, string or int """ - try: return int(s) except ValueError: @@ -134,25 +145,37 @@ def autocast(s): except ValueError: return s + class Attachment(OTRSObject): + """An OTRS attachment.""" + XML_NAME = 'Attachment' + class DynamicField(OTRSObject): + """An OTRS dynamic field.""" + XML_NAME = 'DynamicField' + class AttachmentContainer(object): - """ For objects that can have attachments in them (ex. ticket articles, faq - items) they should inherit this class in addition to OTRSObject + """For objects that can have attachments in them (ex. tickets, articles). + + They should inherit this class in addition to OTRSObject. """ def attachments(self): + """Return the dynamic fields for an object ket as a list. + + @returns a list of Attachment objects. + """ try: return self.childs['Attachment'] except KeyError: return [] def save_attachments(self, folder): - """ Saves the attachments of an article to the specified folder + """Save the attachments of an article to the specified folder. @param folder : a str, folder to save the attachments """ @@ -165,11 +188,18 @@ def save_attachments(self, folder): ffile.write(fcontent) ffile.close() + class DynamicFieldContainer(object): - """ For objects that can have dynamic fields in them (ex. tickets, articles) they should inherit this class in addition to OTRSObject + """For objects that can have dynamic fields in them (ex. tickets, articles). + + They should inherit this class in addition to OTRSObject. """ def dynamicfields(self): + """Return the dynamic fields for an object ket as a list. + + @returns a list of DynamicField objects. + """ try: return self.childs['DynamicField'] except KeyError: @@ -180,11 +210,14 @@ def dynamicfields(self): # with old code that imported these classes from this file # the classes are now in tickets/objects.py + def Ticket(*args, **kwargs): + """Return an OTRS ticket.""" import ticket.objects return ticket.objects.Ticket(*args, **kwargs) + def Article(*args, **kwargs): + """Return an OTRS article.""" import ticket.objects return ticket.objects.Article(*args, **kwargs) - diff --git a/otrs/session/operations.py b/otrs/session/operations.py index 91d72b9..a63219d 100644 --- a/otrs/session/operations.py +++ b/otrs/session/operations.py @@ -1,26 +1,19 @@ -""" Session:: operations +"""OTRS :: session :: operations.""" +from otrs.client import OperationBase -""" - -from ..client import OperationBase, authenticated -from ..objects import extract_tagname class Session(OperationBase): - """ Base class for OTRS Session:: operations + """Base class for OTRS Session:: operations.""" - """ class SessionCreate(Session): - """ Class to handle OTRS Session::SessionCreate operation - - """ + """Class to handle OTRS Session::SessionCreate operation.""" def __call__(self, password, user_login=None, customer_user_login=None): - """ Logs the user or customeruser in + """Create an User session or CustomerUser session. @returns the session_id """ - if user_login: ret = self.req('SessionCreate', UserLogin=user_login, @@ -35,5 +28,6 @@ def __call__(self, password, user_login=None, customer_user_login=None): # sets the session id for the entire client to this self.session_id = session_id - # returns the session id in case you want it, but its not normally needed + # returns the session id in case you want it, + # but its not normally needed return session_id diff --git a/otrs/ticket/objects.py b/otrs/ticket/objects.py index 8663866..2481658 100644 --- a/otrs/ticket/objects.py +++ b/otrs/ticket/objects.py @@ -1,14 +1,26 @@ -from ..objects import OTRSObject, Attachment, DynamicField, AttachmentContainer, DynamicFieldContainer +"""OTRS :: ticket :: objects.""" +from otrs.objects import OTRSObject, Attachment, DynamicField +from otrs.objects import AttachmentContainer, DynamicFieldContainer + class Article(OTRSObject, AttachmentContainer, DynamicFieldContainer): + """An OTRS article.""" + XML_NAME = 'Article' CHILD_MAP = {'Attachment': Attachment, 'DynamicField': DynamicField} + class Ticket(OTRSObject, DynamicFieldContainer): + """An OTRS ticket.""" + XML_NAME = 'Ticket' CHILD_MAP = {'Article': Article, 'DynamicField': DynamicField} def articles(self): + """Return the articles for a ticket as a list. + + @returns a list of Article objects. + """ try: return self.childs['Article'] except KeyError: diff --git a/otrs/ticket/operations.py b/otrs/ticket/operations.py index cc2d4b6..a25317b 100644 --- a/otrs/ticket/operations.py +++ b/otrs/ticket/operations.py @@ -1,26 +1,21 @@ -""" Ticket:: Operations +"""OTRS :: ticket :: operations.""" +from otrs.ticket.objects import Ticket as TicketObject +from otrs.client import OperationBase, authenticated, WrongOperatorException +from otrs.objects import extract_tagname, DynamicField -""" - -from .objects import Ticket as TicketObject -from ..client import OperationBase, authenticated -from ..objects import extract_tagname class Ticket(OperationBase): + """Base class for OTRS Ticket:: operations.""" - """ Base class for OTRS Ticket:: operations - - """ class TicketCreate(Ticket): - """ Class to handle OTRS Ticket::TicketCreate operation - - """ + """Class to handle OTRS Ticket::TicketCreate operation.""" @authenticated def __call__(self, ticket, article, dynamic_fields=None, - attachments=None, **kwargs): - """ + attachments=None, **kwargs): + """Create a new ticket. + @param ticket a Ticket @param article an Article @param dynamic_fields a list of Dynamic Fields @@ -50,15 +45,13 @@ def __call__(self, ticket, article, dynamic_fields=None, class TicketGet(Ticket): - """ Class to handle OTRS Ticket::TicketGet operation - - """ + """Class to handle OTRS Ticket::TicketGet operation.""" @authenticated def __call__(self, ticket_id, get_articles=False, - get_dynamic_fields=False, - get_attachments=False, *args, **kwargs): - """ Get a ticket by id ; beware, TicketID != TicketNumber + get_dynamic_fields=False, + get_attachments=False, *args, **kwargs): + """Get a ticket by id ; beware, TicketID != TicketNumber. @param ticket_id : the TicketID of the ticket @param get_articles : grab articles linked to the ticket @@ -84,13 +77,12 @@ def __call__(self, ticket_id, get_articles=False, class TicketSearch(Ticket): - """ Class to handle OTRS Ticket::TicketSearch operation - - """ + """Class to handle OTRS Ticket::TicketSearch operation.""" @authenticated def __call__(self, dynamic_fields=None, **kwargs): - """ + """Search for a ticket by. + @param dynamic_fields a list of Dynamic Fields, in addition to the combination of `Name` and `Value`, also an `Operator` for the comparison is expexted `Equals`, `Like`, `GreaterThan`, @@ -124,16 +116,16 @@ def __call__(self, dynamic_fields=None, **kwargs): ret = self.req('TicketSearch', **kwargs) return [int(i.text) for i in self._unpack_resp_several(ret)] -class TicketUpdate(Ticket): - """ Class to handle OTRS Ticket::TicketUpdate operation - """ +class TicketUpdate(Ticket): + """Class to handle OTRS Ticket::TicketUpdate operation.""" @authenticated def __call__(self, ticket_id=None, ticket_number=None, - ticket=None, article=None, dynamic_fields=None, - attachments=None, **kwargs): - """ + ticket=None, article=None, dynamic_fields=None, + attachments=None, **kwargs): + """Update an existing ticket. + @param ticket_id the ticket ID of the ticket to modify @param ticket_number the ticket Number of the ticket to modify @param ticket a ticket containing the fields to change on ticket diff --git a/otrs/ticket/template.py b/otrs/ticket/template.py index 6ab9267..1214bfa 100644 --- a/otrs/ticket/template.py +++ b/otrs/ticket/template.py @@ -1,6 +1,17 @@ -from .operations import TicketCreate,TicketGet,TicketSearch,TicketUpdate -from ..session.operations import SessionCreate -from ..client import WebService +"""OTRS :: ticket:: template.""" +from otrs.ticket.operations import TicketCreate, TicketGet +from otrs.ticket.operations import TicketSearch, TicketUpdate +from otrs.session.operations import SessionCreate +from otrs.client import WebService + def GenericTicketConnectorSOAP(webservice_name='GenericTicketConnectorSOAP'): - return WebService(webservice_name, 'http://www.otrs.org/TicketConnector', SessionCreate=SessionCreate(),TicketCreate=TicketCreate(),TicketGet=TicketGet(),TicketSearch=TicketSearch(),TicketUpdate=TicketUpdate()) + """Return a GenericTicketConnectorSOAP Webservice object. + + @returns a WebService object with the GenericTicketConnectorSOAP operations + """ + return WebService(webservice_name, 'http://www.otrs.org/TicketConnector', + SessionCreate=SessionCreate(), + TicketCreate=TicketCreate(), + TicketGet=TicketGet(), TicketSearch=TicketSearch(), + TicketUpdate=TicketUpdate()) From 598fb9136c359f161de603b988c72e9ca800c03b Mon Sep 17 00:00:00 2001 From: EWSterrenburg Date: Mon, 18 Jul 2016 10:15:03 +0200 Subject: [PATCH 8/9] Improved some doclines as pointed out by MJDucharme Properly included ssl_context. --- otrs/client.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/otrs/client.py b/otrs/client.py index 93ca8dc..f33fd53 100644 --- a/otrs/client.py +++ b/otrs/client.py @@ -126,19 +126,24 @@ def password(self): """Return password attribute of the clientobject of the WebService.""" return self.getClientObjectAttribute('password') + @property + def ssl_context(self): + """Return ssl_context of the clientobject of the WebService.""" + return self.getClientObjectAttribute('ssl_context') + @property def session_id(self): - """Return session_id of WebService object.""" + """Return session_id of the clientobject of the WebService object.""" return self.getClientObjectAttribute('session_id') @session_id.setter def session_id(self, sessionid): - """Set session_id of WebService object.""" + """Set session_id of the clientobject of the WebService object.""" self.setClientObjectAttribute('session_id', sessionid) @property def soap_envelope(self): - """Return session_id of WebService object.""" + """Return soap envelope for WebService object.""" soap_envelope = ' Date: Tue, 19 Jul 2016 16:07:56 +0200 Subject: [PATCH 9/9] Adapted tests to work with rewritten code. --- otrs/objects.py | 8 ++++---- tests.py | 43 ++++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/otrs/objects.py b/otrs/objects.py index 9bc9796..65a8c5e 100644 --- a/otrs/objects.py +++ b/otrs/objects.py @@ -213,11 +213,11 @@ def dynamicfields(self): def Ticket(*args, **kwargs): """Return an OTRS ticket.""" - import ticket.objects - return ticket.objects.Ticket(*args, **kwargs) + import otrs.ticket.objects + return otrs.ticket.objects.Ticket(*args, **kwargs) def Article(*args, **kwargs): """Return an OTRS article.""" - import ticket.objects - return ticket.objects.Article(*args, **kwargs) + import otrs.ticket.objects + return otrs.ticket.objects.Article(*args, **kwargs) diff --git a/tests.py b/tests.py index 9d6c4c3..23968f3 100644 --- a/tests.py +++ b/tests.py @@ -2,8 +2,9 @@ import os import xml.etree.ElementTree as etree -from otrs.client import GenericTicketConnector -from otrs.objects import Ticket, Article +from otrs.client import GenericInterfaceClient +from otrs.ticket.template import GenericTicketConnectorSOAP +from otrs.ticket.objects import Ticket, Article REQUIRED_VARS = 'OTRS_LOGIN', 'OTRS_PASSWORD', 'OTRS_SERVER', 'OTRS_WEBSERVICE' MISSING_VARS = [] @@ -168,21 +169,21 @@ class TestOTRSAPI(unittest.TestCase): def setUp(self): - self.c = GenericTicketConnector(OTRS_SERVER, OTRS_WEBSERVICE) + self.c = GenericInterfaceClient(OTRS_SERVER, tc=GenericTicketConnectorSOAP(OTRS_WEBSERVICE)) self.c.register_credentials(OTRS_LOGIN, OTRS_PASSWORD) def test_session_create(self): - sessid = self.c.session_create(user_login=OTRS_LOGIN, - password=OTRS_PASSWORD) + sessid = self.c.tc.SessionCreate(user_login=OTRS_LOGIN, + password=OTRS_PASSWORD) self.assertEqual(len(sessid), 32) def test_ticket_get(self): - t = self.c.ticket_get(1) + t = self.c.tc.TicketGet(1) self.assertEqual(t.TicketID, 1) self.assertEqual(t.StateType, 'new') def test_ticket_get_with_articles(self): - t = self.c.ticket_get(1, get_articles=True) + t = self.c.tc.TicketGet(1, get_articles=True) self.assertEqual(t.TicketID, 1) self.assertEqual(t.StateType, 'new') articles = t.articles() @@ -191,7 +192,7 @@ def test_ticket_get_with_articles(self): self.assertEqual(articles[0].SenderType, 'customer') def test_ticket_search(self): - t_list = self.c.ticket_search(Title='Welcome to OTRS!') + t_list = self.c.tc.TicketSearch(Title='Welcome to OTRS!') self.assertIsInstance(t_list, list) self.assertIn(1, t_list) @@ -206,7 +207,7 @@ def test_ticket_create(self): Body='bla', Charset='UTF8', MimeType='text/plain') - t_id, t_number = self.c.ticket_create(t, a) + t_id, t_number = self.c.tc.TicketCreate(t, a) self.assertIsInstance(t_id, int) self.assertIsInstance(t_number, int) self.assertTrue(len(str(t_number)) >= 12) @@ -223,11 +224,11 @@ def test_ticket_update_attrs_by_id(self): Body='bla', Charset='UTF8', MimeType='text/plain') - t_id, t_number = self.c.ticket_create(t, a) + t_id, t_number = self.c.tc.TicketCreate(t, a) t = Ticket(Title='Foubar') - upd_tid, upd_tnumber = self.c.ticket_update(ticket_id=t_id, - ticket=t) + upd_tid, upd_tnumber = self.c.tc.TicketUpdate(ticket_id=t_id, + ticket=t) self.assertIsInstance(upd_tid, int) self.assertIsInstance(upd_tnumber, int) self.assertTrue(len(str(upd_tnumber)) >= 12) @@ -235,7 +236,7 @@ def test_ticket_update_attrs_by_id(self): self.assertEqual(upd_tid, t_id) self.assertEqual(upd_tnumber, t_number) - upd_t = self.c.ticket_get(t_id) + upd_t = self.c.tc.TicketGet(t_id) self.assertEqual(upd_t.Title, 'Foubar') self.assertEqual(upd_t.Queue, 'Postmaster') @@ -250,11 +251,11 @@ def test_ticket_update_attrs_by_number(self): Body='bla', Charset='UTF8', MimeType='text/plain') - t_id, t_number = self.c.ticket_create(t, a) + t_id, t_number = self.c.tc.TicketCreate(t, a) t = Ticket(Title='Foubar') - upd_tid, upd_tnumber = self.c.ticket_update(ticket_number=t_number, - ticket=t) + upd_tid, upd_tnumber = self.c.tc.TicketUpdate(ticket_number=t_number, + ticket=t) self.assertIsInstance(upd_tid, int) self.assertIsInstance(upd_tnumber, int) self.assertTrue(len(str(upd_tnumber)) >= 12) @@ -262,7 +263,7 @@ def test_ticket_update_attrs_by_number(self): self.assertEqual(upd_tid, t_id) self.assertEqual(upd_tnumber, t_number) - upd_t = self.c.ticket_get(t_id) + upd_t = self.c.tc.TicketGet(t_id) self.assertEqual(upd_t.Title, 'Foubar') self.assertEqual(upd_t.Queue, 'Postmaster') @@ -277,7 +278,7 @@ def test_ticket_update_new_article(self): Body='bla', Charset='UTF8', MimeType='text/plain') - t_id, t_number = self.c.ticket_create(t, a) + t_id, t_number = self.c.tc.TicketCreate(t, a) a2 = Article(Subject='UnitTest2', Body='bla', @@ -289,10 +290,10 @@ def test_ticket_update_new_article(self): Charset='UTF8', MimeType='text/plain') - self.c.ticket_update(t_id, article=a2) - self.c.ticket_update(t_id, article=a3) + self.c.tc.TicketUpdate(t_id, article=a2) + self.c.tc.TicketUpdate(t_id, article=a3) - t_upd = self.c.ticket_get(t_id, get_articles=True) + t_upd = self.c.tc.TicketGet(t_id, get_articles=True) arts_upd = t_upd.articles() self.assertIsInstance(arts_upd, list) self.assertEqual(len(arts_upd), 3)