# -*- 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 . # ################################################################################ """ 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. .. note:: Currently this is being used to create a *new* vendor also. CORE's ``vendors`` table does not use auto-increment for its PK, which means we must provide one even when creating; therefore this method may be used for that. """ columns['vendorID'] = vendorID params = { 'entity': 'Vendors', 'submethod': 'set', 'columns': columns, } response = self.post(params) result = self.parse_response(response) return json.loads(result)