Add basic CORE webservices API client, for Vendor data
lots more to come yet, once the basic patterns are proven
This commit is contained in:
		
							parent
							
								
									a0efa1a967
								
							
						
					
					
						commit
						7fc5ae9b4e
					
				
					 2 changed files with 178 additions and 1 deletions
				
			
		
							
								
								
									
										174
									
								
								corepos/api.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								corepos/api.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,174 @@
 | 
			
		|||
# -*- coding: utf-8; -*-
 | 
			
		||||
################################################################################
 | 
			
		||||
#
 | 
			
		||||
#  pyCOREPOS -- Python Interface to CORE POS
 | 
			
		||||
#  Copyright © 2018-2020 Lance Edgar
 | 
			
		||||
#
 | 
			
		||||
#  This file is part of pyCOREPOS.
 | 
			
		||||
#
 | 
			
		||||
#  pyCOREPOS is free software: you can redistribute it and/or modify it under
 | 
			
		||||
#  the terms of the GNU General Public License as published by the Free
 | 
			
		||||
#  Software Foundation, either version 3 of the License, or (at your option)
 | 
			
		||||
#  any later version.
 | 
			
		||||
#
 | 
			
		||||
#  pyCOREPOS is distributed in the hope that it will be useful, but WITHOUT ANY
 | 
			
		||||
#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 | 
			
		||||
#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 | 
			
		||||
#  details.
 | 
			
		||||
#
 | 
			
		||||
#  You should have received a copy of the GNU General Public License along with
 | 
			
		||||
#  pyCOREPOS.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
#
 | 
			
		||||
################################################################################
 | 
			
		||||
"""
 | 
			
		||||
CORE-POS webservices API
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoreAPIError(Exception):
 | 
			
		||||
    """
 | 
			
		||||
    Base class for errors coming from the CORE API proper.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, message):
 | 
			
		||||
        self.message = message
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return "CORE API returned an error: {}".format(self.message)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CoreWebAPI(object):
 | 
			
		||||
    """
 | 
			
		||||
    Client implementation for the CORE webservices API.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, url, verify=True):
 | 
			
		||||
        """
 | 
			
		||||
        Constructor for the API client.
 | 
			
		||||
 | 
			
		||||
        :param str url: URL to the CORE webservices API,
 | 
			
		||||
           e.g. ``'http://localhost/fannie/ws/'``
 | 
			
		||||
 | 
			
		||||
        :param bool verify: How to handle certificate validation for HTTPS
 | 
			
		||||
           URLs.  This value is passed as-is to the ``requests`` library, so
 | 
			
		||||
           see those docs for more info.  The default value for this is
 | 
			
		||||
           ``True`` because the assumption is that security should be on by
 | 
			
		||||
           default.  Set it to ``False`` in order to disable validation
 | 
			
		||||
           entirely, e.g. for self-signed certs.  (This may also be needed for
 | 
			
		||||
           basic HTTP URLs?)  Other values may be possible also; again see the
 | 
			
		||||
           ``requests`` docs for more info.
 | 
			
		||||
        """
 | 
			
		||||
        self.url = url
 | 
			
		||||
        self.verify = verify
 | 
			
		||||
 | 
			
		||||
    def post(self, params, method=None):
 | 
			
		||||
        """
 | 
			
		||||
        Issue a POST request to the API, with the given ``params``.  If not
 | 
			
		||||
        specified, ``method`` will be CORE's ``FannieEntity`` webservice.
 | 
			
		||||
        """
 | 
			
		||||
        if not method:
 | 
			
		||||
            method = r'\COREPOS\Fannie\API\webservices\FannieEntity'
 | 
			
		||||
 | 
			
		||||
        payload = {
 | 
			
		||||
            'jsonrpc': '2.0',
 | 
			
		||||
            'method': method,
 | 
			
		||||
            'params': params,
 | 
			
		||||
            # we're not dealing with async here, so KISS for this 'id'
 | 
			
		||||
            # https://stackoverflow.com/questions/4390369/json-rpc-how-can-one-make-a-unique-id#comment4786119_4391070
 | 
			
		||||
            'id': 1,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        response = requests.post(self.url, data=json.dumps(payload),
 | 
			
		||||
                                 verify=self.verify)
 | 
			
		||||
        response.raise_for_status()
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    def parse_response(self, response):
 | 
			
		||||
        """
 | 
			
		||||
        Generic method to "parse" a response from the API.  Really this just
 | 
			
		||||
        converts the JSON to a dict (etc.), and then checks for error.  If an
 | 
			
		||||
        error is found in the response, it will be raised here.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            js = response.json()
 | 
			
		||||
        except:
 | 
			
		||||
            raise CoreAPIError("Received invalid response: {}".format(response.content))
 | 
			
		||||
 | 
			
		||||
        if 'error' in js:
 | 
			
		||||
            raise CoreAPIError(js['error'])
 | 
			
		||||
 | 
			
		||||
        assert set(js.keys()) == set(['jsonrpc', 'id', 'result'])
 | 
			
		||||
        assert set(js['result'].keys()) == set(['result'])
 | 
			
		||||
        return js['result']['result']
 | 
			
		||||
 | 
			
		||||
    def get_vendors(self, **columns):
 | 
			
		||||
        """
 | 
			
		||||
        Fetch some or all of Vendor records from CORE.
 | 
			
		||||
 | 
			
		||||
        :returns: A (potentially empty) list of vendor dict records.
 | 
			
		||||
 | 
			
		||||
        To fetch all vendors::
 | 
			
		||||
 | 
			
		||||
           api.get_vendors()
 | 
			
		||||
 | 
			
		||||
        To fetch only vendors named "UNFI"::
 | 
			
		||||
 | 
			
		||||
           api.get_vendors(vendorName='UNFI')
 | 
			
		||||
        """
 | 
			
		||||
        params = {
 | 
			
		||||
            'entity': 'Vendors',
 | 
			
		||||
            'submethod': 'get',
 | 
			
		||||
            'columns': columns,
 | 
			
		||||
        }
 | 
			
		||||
        response = self.post(params)
 | 
			
		||||
        result = self.parse_response(response)
 | 
			
		||||
        return [json.loads(rec) for rec in result]
 | 
			
		||||
 | 
			
		||||
    def get_vendor(self, vendorID, **columns):
 | 
			
		||||
        """
 | 
			
		||||
        Fetch an existing Vendor record from CORE.
 | 
			
		||||
 | 
			
		||||
        :returns: Either a vendor dict record, or ``None``.
 | 
			
		||||
        """
 | 
			
		||||
        columns['vendorID'] = vendorID
 | 
			
		||||
        params = {
 | 
			
		||||
            'entity': 'Vendors',
 | 
			
		||||
            'submethod': 'get',
 | 
			
		||||
            'columns': columns,
 | 
			
		||||
        }
 | 
			
		||||
        response = self.post(params)
 | 
			
		||||
        result = self.parse_response(response)
 | 
			
		||||
        if result:
 | 
			
		||||
            if len(result) > 1:
 | 
			
		||||
                log.warning("CORE API returned %s vendor results", len(result))
 | 
			
		||||
            return json.loads(result[0])
 | 
			
		||||
 | 
			
		||||
    def set_vendor(self, vendorID, **columns):
 | 
			
		||||
        """
 | 
			
		||||
        Update an existing Vendor record in CORE.
 | 
			
		||||
 | 
			
		||||
        :returns: Boolean indicating success of the operation.
 | 
			
		||||
        """
 | 
			
		||||
        columns['vendorID'] = vendorID
 | 
			
		||||
        params = {
 | 
			
		||||
            'entity': 'Vendors',
 | 
			
		||||
            'submethod': 'set',
 | 
			
		||||
            'columns': columns,
 | 
			
		||||
        }
 | 
			
		||||
        response = self.post(params)
 | 
			
		||||
        result = self.parse_response(response)
 | 
			
		||||
 | 
			
		||||
        if result == 'OK':
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        # TODO: need to see what happens here, when it happens
 | 
			
		||||
        raise NotImplementedError("Unexpected result for set_vendor: {}".format(result))
 | 
			
		||||
        # return False
 | 
			
		||||
| 
						 | 
				
			
			@ -137,7 +137,10 @@ class Vendor(Base):
 | 
			
		|||
    """
 | 
			
		||||
    __tablename__ = 'vendors'
 | 
			
		||||
    
 | 
			
		||||
    id = sa.Column('vendorID', sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
 | 
			
		||||
    # TODO: this maybe should be the pattern we use going forward, for all
 | 
			
		||||
    # models?  for now it was deemed necessary to "match" the API output
 | 
			
		||||
    vendorID = sa.Column(sa.Integer(), primary_key=True, autoincrement=False, nullable=False)
 | 
			
		||||
    id = orm.synonym('vendorID')
 | 
			
		||||
 | 
			
		||||
    name = sa.Column('vendorName', sa.String(length=50), nullable=True)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue