Skip to content

russellmcdonell/DecisionCentral

Repository files navigation

DecisionCentral

DecisionCentral is a central repository for all DMN based decision services.

DecisionCentral

  • Creates a web site that lets you upload DMN compliant Excel workbooks or DMN conformant XML files
  • For each uploaded DMN compliant Excel workbook or DMN compliant XML file, DecisionCentral will
    • create a decision service from that DMN compliant Excel workbook or DMN conformant XML file
    • create a user interface where users can enter values and check specific use cases for this decision service
    • creates an API for this decision service which accepts JSON data (the input values) and returns a JSON structure (representing the decision)
  • For each decision service, DecisionCentral will create web pages detailing all the parts of your decision service
    • The glossary of data items, both inputs and outputs, associated with this decision service
    • The decision sequence (the sequence in which you decision tables will be run)
    • The decision tables that form this decision service
    • An OpenAPI specification for the the API associated with this decision service which will be displayed as a web page, but it can also be downloaded and imported to Postman/Swagger etc.
  • For each decision table, within each decision service, DecisionCentral will
    • create a DMN compliant representation of the rules built when the decision service was created
    • create a user interface where users can enter values and check specific use cases for this decision table within this decision service
    • create an API for this decision table within this decision service which accepts JSON data (the input values) and returns a JSON structure (representing the decision)
    • createe an OpenAPI specification for the the API associated with this decision table with this decision service which will be displayed as a web page, but it can also be downloaded and imported to Postman/Swagger etc.

DecisionCentral also has an API for uploading a DMN compliant Excel workbook or DMN conformant XML file, plus and API for deleting a decision service.

DecisionCentral listens for http requests on port 7777 by default. The -p portNo option lets you assign a different port. However, DecisionCental can also be run in a container (it uses no disk storage - see the dockerfile) and you can use containter port mapping to map your desired port to 7777.

DecisionCentral can be run locally (see -h option for details).
However can also be run in a container - dockerfile can be used to build a Docker image
$ docker build -t decisioncentral:0.0.1 .
And run under Docker Desktop
$ docker run --name decisioncentral -p 7777:7777 -d decisioncentral:0.0.1

There is a flask version of DecisionCentral which is the reference version. It can also be run in a docker container and the basis for [DecisionCentralAzure] (https://github.com/russellmcdonell/DecisionCentralAzure) - a version which creates an Azure Program as a Platform instance of DecisionCentral. NOTE: The flask versions listens for http requests on port 5000, and it too can be run in a container.

JSON and DMN data types JSON doesn't support all the data types that are supported by DMN (and the FEEL expression language). Decision Central borrows a solution suggested by FEEL - @strings. If a string starts with the two characters @" and ends with the character " then what is in between has to be de-serialized by some other non-JSON interpreter. In this case, the FEEL interpreter (pySFeel). The following code can be used to serialize and de-serialize @strings.

import datetime
import pySFeel

parser = pySFeel.SFeelParser()


def convertAtString(thisString):
    # Convert an @string
    (status, newValue) = parser.sFeelParse(thisString[2:-1])
    if 'errors' in status:
        return thisString
    else:
        return newValue


def convertIn(newValue):
    if isinstance(newValue, dict):
        for key in newValue:
            if isinstance(newValue[key], int):
                newValue[key] = float(newValue[key])
            elif isinstance(newValue[key], str) and (newValue[key][0:2] == '@"') and (newValue[key][-1] == '"'):
                newValue[key] = convertAtString(newValue[key])
            elif isinstance(newValue[key], dict) or isinstance(newValue[key], list):
                newValue[key] = convertIn(newValue[key])
    elif isinstance(newValue, list):
        for i in range(len(newValue)):
            if isinstance(newValue[i], int):
                newValue[i] = float(newValue[i])
            elif isinstance(newValue[i], str) and (newValue[i][0:2] == '@"') and (newValue[i][-1] == '"'):
                newValue[i] = convertAtString(newValue[i])
            elif isinstance(newValue[i], dict) or isinstance(newValue[i], list):
                newValue[i] = convertIn(newValue[i])
    elif isinstance(newValue, str) and (newValue[0:2] == '@"') and (newValue[-1] == '"'):
        newValue = convertAtString(newValue)
    return newValue


  def convertOut(thisValue):
      if isinstance(thisValue, datetime.date):
          return '@"' + thisValue.isoformat() + '"'
      elif isinstance(thisValue, datetime.datetime):
          return '@"' + thisValue.isoformat(sep='T') + '"'
      elif isinstance(thisValue, datetime.time):
          return '@"' + thisValue.isoformat() + '"'
      elif isinstance(thisValue, datetime.timedelta):
          sign = ''
          duration = thisValue.total_seconds()
          if duration < 0:
              duration = -duration
              sign = '-'
          secs = duration % 60
          duration = int(duration / 60)
          mins = duration % 60
          duration = int(duration / 60)
          hours = duration % 24
          days = int(duration / 24)
          return '@"%sP%dDT%dH%dM%fS"' % (sign, days, hours, mins, secs)
      elif isinstance(thisValue, bool):
          return thisValue:
      elif thisValue is None:
          return thisValue:
      elif isinstance(thisValue, int):
          sign = ''
          if thisValue < 0:
              thisValue = -thisValue
              sign = '-'
          years = int(thisValue / 12)
          months = (thisValue % 12)
          return '@"%sP%dY%dM"' % (sign, years, months)
      elif isinstance(thisValue, tuple) and (len(thisValue) == 4):
          (lowEnd, lowVal, highVal, highEnd) = thisValue
          return '@"' + lowEnd + str(lowVal) + ' .. ' + str(highVal) + highEnd
      elif thisValue is None:
          return 'null'
      elif isinstance(thisValue, dict):
          for item in thisValue:
              thisValue[item] = convertOut(thisValue[item])
          return thisValue
      elif isinstance(thisValue, list):
          for i in range(len(thisValue)):
              thisValue[i] = convertOut(thisValue[i])
          return thisValue
      else:
          return thisValue

questioner.py is a client that calls a specified Decision Central API, passing data from questions.xlsx and storing the decisions in answers.xlsx

DecisionCentral is not, of itself, a production product. You use pyDMNrules to build those.
It is intended for use at Hackathons and Connectathons; anywhere you need a complex decision service created quickly and easily.