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
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'
|
__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)
|
name = sa.Column('vendorName', sa.String(length=50), nullable=True)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue