mcphargus / python-glpi

A python interface to the GLPI webservices plugin
GNU General Public License v2.0
15 stars 9 forks source link

code proposition : RESTClient refactoring #4

Closed frague59 closed 11 years ago

frague59 commented 11 years ago

Hi,

From your code, I've created a new way to access to the GLPI service. I've refactored the connexion / aithentification process and have curryed all glpi methods.

!/usr/bin/env python

-- coding:utf-8 --

import urllib, urllib2 import json import sys, os, warnings

import getpass

import logging

DEBUG = True

logging.basicConfig(level=logging.DEBUG)

def curry(_curried_func, _args, _kwargs): def _curried(_moreargs, _morekwargs): return _curriedfunc((args + moreargs), *_dict(kwargs, **morekwargs)) return _curried

class RESTClient(object): """ .. note:: If any boolean arguments are defined, they're automatically added to the GET request, which means the webservices API will treat them as being true. You've been warned. """

# List all methods defined in the REST API
METHODS = dict(test='glpi.test',
               status='glpi.status',
               listAllMethods='glpi.listAllMethods',
               listEntities='glpi.listEntities',
               doLogin='glpi.doLogin',
               listKnowBaseItems='glpi.listKnowBaseItems',
               getKnowBaseItem='glpi.getKnowBaseItem',
               getDocument='glpi.getDocument',
               doLogout='glpi.doLogout',
               getMyInfo='glpi.getMyInfo',
               listMyProfiles='glpi.listMyProfiles',
               setMyProfile='glpi.setMyProfile',
               listMyEntities='glpi.listMyEntities',
               setMyEntity='glpi.setMyEntity',
               listDropdownValues='glpi.listDropdownValues',
               listGroups='glpi.listGroups',
               listHelpdeskTypes='glpi.listHelpdeskTypes',
               listHelpdeskItems='glpi.listHelpdeskItems',
               listTickets='glpi.listTickets',
               listUsers='glpi.listUsers',
               listInventoryObjects='glpi.listInventoryObjects',
               listObjects='glpi.listObjects',
               getObject='glpi.getObject',
               createObjects='glpi.createObjects',
               deleteObjects='glpi.deleteObjects',
               updateObjects='glpi.updateObjects',
               linkObjects='glpi.linkObjects',
               getInfocoms='glpi.getInfocoms',
               getContracts='glpi.getContracts',
               getComputer='glpi.getComputer',
               getComputerInfoComs='glpi.getComputerInfoComs',
               getComputerContracts='glpi.getComputerContracts',
               getNetworkports='glpi.getNetworkports',
               listComputers='glpi.listComputers',
               getPhones='glpi.getPhones',
               getNetworkEquipment='glpi.getNetworkEquipment',
               getTicket='glpi.getTicket',
               createTicket='glpi.createTicket',
               addTicketFollowup='glpi.addTicketFollowup',
               addTicketDocument='glpi.addTicketDocument',
               addTicketObserver='glpi.addTicketObserver',
               setTicketSatisfaction='glpi.setTicketSatisfaction',
               setTicketValidation='glpi.setTicketValidation',)

def __init__(self, host='glpi', baseurl='/glpi',):
    '''
    Initialize the RESTClient instance, adding supported glpi-methods by currying self._get_METHOD
    '''
    self.BASEURL = baseurl
    self._url = 'http://' + host + '/' + self.BASEURL + '/plugins/webservices/rest.php?'
    for method in self.METHODS.keys():
        setattr(self, 'get_%s' % (method),
                curry(self._get_METHOD, self.METHODS.get(method)))

def _get_METHOD(self, method, **kwargs):
    '''

    Private method that perform the call to glpi server with the glpi method
    and providing parameters as kwargs
    @param method: GLPI method to call
    @param kwargs: parameters to send to glpi test
    '''
    if not self.connected:
        self.connect(login_name='webservices', login_password='webservices')
    response = self.send_request(method, **kwargs)
    DEBUG and logging.debug(u'RESTClient::get_%s() '
                  u'response = %s'
                  % (method, response,))

    return response

# Attributes
_session = None
_url = None

@property
def session(self):
    '''
    The session string
    '''
    return self._session

@property
def connected(self):
    '''
    tests in client is connected to the server
    '''
    return self.session is not None

@property
def url(self):
    ''' The complete url '''
    return self._url

@staticmethod
def _is_fault(response):
    '''
    tests if a fault has been raised by the server
    @param response: the decoded respose object
    @return: False if there is no error
    @raise Exception: if a server side error has been raised
    '''
    DEBUG and logging.debug(u'RESTClient::_is_fault() '
                 u'response = %s'
                 % (response,))

    if (isinstance(response, (dict))):
        faultCode = response.get('faultCode')
        faultString = response.get('faultString')
        if (faultCode is not None):
            raise Exception('Fault returned by GLPI service : %s : %s' % (faultCode, faultString,))
    # There is no server-side fault.
    return False

def _build_url(self, method, params=None):
    '''
    private method to build the final url sent to the server
    @param method: the glpi method
    @param params: params to join to the request
    @return: the final url that will be sent
    '''
    _params = {"method": method, }
    if params:
        _params.update(params)
    _url = self.url + urllib.urlencode(_params)
    logging.debug(u'RESTClient::_build_url(%s, %s) '
                 u'_url = %s '
                 % (method, params, _url,))
    return _url

def send_request(self, method, **kwargs):
    '''
    send a request to GLPI server and returns the json-decoded response

    @param method: glpi-method
    @param kwargs: extra-parameters to pass to the method
    @return: response object json-decoded
    @raise Exception: if the server raises {'faultCode', 'faultString'}
        or if something has failed.
    '''
    self.connected and logging.debug(u'RESTClient::get(%s, %s) '
                                     u'CONNECTED !'
                                     % (method, kwargs,))
    # Prepares parameters
    _params = {'method': method, }
    if kwargs:
        _params.update(kwargs)

    # Append session to parameters
    if self.session:
        _params['session'] = self.session

    url = self._build_url(method, _params)
    request = urllib2.Request(url)

    # Sends the request
    try:
        response = urllib2.urlopen(request)
        headers = response.headers
        read = response.read()
        logging.debug(u'RESTClient::send_request(%s, %s) '
                 u'response: %s /'
                 % (method, kwargs, read,))
        json_decoded = json.loads(read)
        if not RESTClient._is_fault(json_decoded):  # An exception is raised on protocol or glpi method error
            return json_decoded
    except Exception as e:
        # Exception is re-raised
        raise e

def connect(self, login_name=None, login_password=None):
    """
    Connect to a running GLPI instance that has the webservices
    plugin enabled.

    Returns True if connection was successful.

    :type login_name: string
    :type login_password: string
    :param host: hostname of the GLPI server, has not been tested with HTTPS
    :param login_name: your GLPI username
    :param login_password: pretty obvious
    """
    self.login_name = login_name
    self.login_password = login_password

    if self.login_name != None and self.login_password != None:
        params = {'login_name':login_name,
                  'login_password': (login_password), }
        response = self.send_request("glpi.doLogin", **params)
        session_id = response.get('session')
        if session_id is not None:
            self._session = session_id
            return True
        else:
            raise Exception("Login incorrect or server down")
    else:
        logging.warning("Connected anonymously, will only be able to use non-authenticated methods")
        return self

The curry function is copied from the django project.

To access to th glpi.method, you just have to call the get_method which has been dynalicaly created on init.

I let you read the code and tell me what you think about it.

Cordialy,

frague

PS : english is not my first language, so excuse for english faults...

frague59 commented 11 years ago

Code : ''' python

!/usr/bin/env python

-- coding:utf-8 --

import urllib, urllib2 import json import sys, os, warnings

import getpass

import logging

DEBUG = True

logging.basicConfig(level=logging.DEBUG)

def curry(_curried_func, _args, _kwargs): def _curried(_moreargs, _morekwargs): return _curriedfunc((args + moreargs), *_dict(kwargs, **morekwargs)) return _curried

class RESTClient(object): """ .. note:: If any boolean arguments are defined, they're automatically added to the GET request, which means the webservices API will treat them as being true. You've been warned. """

# List all methods defined in the REST API
METHODS = dict(test='glpi.test',
               status='glpi.status',
               listAllMethods='glpi.listAllMethods',
               listEntities='glpi.listEntities',
               doLogin='glpi.doLogin',
               listKnowBaseItems='glpi.listKnowBaseItems',
               getKnowBaseItem='glpi.getKnowBaseItem',
               getDocument='glpi.getDocument',
               doLogout='glpi.doLogout',
               getMyInfo='glpi.getMyInfo',
               listMyProfiles='glpi.listMyProfiles',
               setMyProfile='glpi.setMyProfile',
               listMyEntities='glpi.listMyEntities',
               setMyEntity='glpi.setMyEntity',
               listDropdownValues='glpi.listDropdownValues',
               listGroups='glpi.listGroups',
               listHelpdeskTypes='glpi.listHelpdeskTypes',
               listHelpdeskItems='glpi.listHelpdeskItems',
               listTickets='glpi.listTickets',
               listUsers='glpi.listUsers',
               listInventoryObjects='glpi.listInventoryObjects',
               listObjects='glpi.listObjects',
               getObject='glpi.getObject',
               createObjects='glpi.createObjects',
               deleteObjects='glpi.deleteObjects',
               updateObjects='glpi.updateObjects',
               linkObjects='glpi.linkObjects',
               getInfocoms='glpi.getInfocoms',
               getContracts='glpi.getContracts',
               getComputer='glpi.getComputer',
               getComputerInfoComs='glpi.getComputerInfoComs',
               getComputerContracts='glpi.getComputerContracts',
               getNetworkports='glpi.getNetworkports',
               listComputers='glpi.listComputers',
               getPhones='glpi.getPhones',
               getNetworkEquipment='glpi.getNetworkEquipment',
               getTicket='glpi.getTicket',
               createTicket='glpi.createTicket',
               addTicketFollowup='glpi.addTicketFollowup',
               addTicketDocument='glpi.addTicketDocument',
               addTicketObserver='glpi.addTicketObserver',
               setTicketSatisfaction='glpi.setTicketSatisfaction',
               setTicketValidation='glpi.setTicketValidation',)

def __init__(self, host='glpi', baseurl='/glpi',):
    '''
    Initialize the RESTClient instance, adding supported glpi-methods by currying self._get_METHOD
    '''
    self.BASEURL = baseurl
    self._url = 'http://' + host + '/' + self.BASEURL + '/plugins/webservices/rest.php?'
    for method in self.METHODS.keys():
        setattr(self, 'get_%s' % (method),
                curry(self._get_METHOD, self.METHODS.get(method)))

def _get_METHOD(self, method, **kwargs):
    '''

    Private method that perform the call to glpi server with the glpi method
    and providing parameters as kwargs
    @param method: GLPI method to call
    @param kwargs: parameters to send to glpi test
    '''
    if not self.connected:
        self.connect(login_name='webservices', login_password='webservices')
    response = self.send_request(method, **kwargs)
    DEBUG and logging.debug(u'RESTClient::get_%s() '
                  u'response = %s'
                  % (method, response,))

    return response

# Attributes
_session = None
_url = None

@property
def session(self):
    '''
    The session string
    '''
    return self._session

@property
def connected(self):
    '''
    tests in client is connected to the server
    '''
    return self.session is not None

@property
def url(self):
    ''' The complete url '''
    return self._url

@staticmethod
def _is_fault(response):
    '''
    tests if a fault has been raised by the server
    @param response: the decoded respose object
    @return: False if there is no error
    @raise Exception: if a server side error has been raised
    '''
    DEBUG and logging.debug(u'RESTClient::_is_fault() '
                 u'response = %s'
                 % (response,))

    if (isinstance(response, (dict))):
        faultCode = response.get('faultCode')
        faultString = response.get('faultString')
        if (faultCode is not None):
            raise Exception('Fault returned by GLPI service : %s : %s' % (faultCode, faultString,))
    # There is no server-side fault.
    return False

def _build_url(self, method, params=None):
    '''
    private method to build the final url sent to the server
    @param method: the glpi method
    @param params: params to join to the request
    @return: the final url that will be sent
    '''
    _params = {"method": method, }
    if params:
        _params.update(params)
    _url = self.url + urllib.urlencode(_params)
    logging.debug(u'RESTClient::_build_url(%s, %s) '
                 u'_url = %s '
                 % (method, params, _url,))
    return _url

def send_request(self, method, **kwargs):
    '''
    send a request to GLPI server and returns the json-decoded response

    @param method: glpi-method
    @param kwargs: extra-parameters to pass to the method
    @return: response object json-decoded
    @raise Exception: if the server raises {'faultCode', 'faultString'}
        or if something has failed.
    '''
    self.connected and logging.debug(u'RESTClient::get(%s, %s) '
                                     u'CONNECTED !'
                                     % (method, kwargs,))
    # Prepares parameters
    _params = {'method': method, }
    if kwargs:
        _params.update(kwargs)

    # Append session to parameters
    if self.session:
        _params['session'] = self.session

    url = self._build_url(method, _params)
    request = urllib2.Request(url)

    # Sends the request
    try:
        response = urllib2.urlopen(request)
        headers = response.headers
        read = response.read()
        logging.debug(u'RESTClient::send_request(%s, %s) '
                 u'response: %s /'
                 % (method, kwargs, read,))
        json_decoded = json.loads(read)
        if not RESTClient._is_fault(json_decoded):  # An exception is raised on protocol or glpi method error
            return json_decoded
    except Exception as e:
        # Exception is re-raised
        raise e

def connect(self, login_name=None, login_password=None):
    """
    Connect to a running GLPI instance that has the webservices
    plugin enabled.

    Returns True if connection was successful.

    :type login_name: string
    :type login_password: string
    :param host: hostname of the GLPI server, has not been tested with HTTPS
    :param login_name: your GLPI username
    :param login_password: pretty obvious
    """
    self.login_name = login_name
    self.login_password = login_password

    if self.login_name != None and self.login_password != None:
        params = {'login_name':login_name,
                  'login_password': (login_password), }
        response = self.send_request("glpi.doLogin", **params)
        session_id = response.get('session')
        if session_id is not None:
            self._session = session_id
            return True
        else:
            raise Exception("Login incorrect or server down")
    else:
        logging.warning("Connected anonymously, will only be able to use non-authenticated methods")
        return self

'''

mcphargus commented 11 years ago

At first glance this seems awesome. currying is a concept that I hadn't heard of until now. I'll check this out when I have some more time to fully understand it. Looks cool, I'll probably add it to the current codebase or set you up as a contributor, if you'd like.

I'll admit, this is the first open source project that I've maintained that has generated any interest, so I'm a little fuzzy on the best way to manage things.

And I'm sorry that I don't speak any languages other than English, I'll do my best to keep up where I can.

I'm reopening this ticket, and once this has been fully integrated into the current master branch, I'll close this issue.