pyDMNrules-enhanced 1.5.0__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/LICENSE_MCP +1 -0
  2. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/MANIFEST.in +1 -0
  3. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/MCP_PROJECT_SUMMARY.md +1 -0
  4. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/PKG-INFO +3 -13
  5. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/SETUP_GUIDE.md +1 -0
  6. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules/DMNrules.py +151 -30
  7. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules_enhanced.egg-info/PKG-INFO +3 -13
  8. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pydmnrules_mcp/__init__.py +1 -0
  9. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/requirements_mcp.txt +1 -0
  10. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/setup.py +2 -2
  11. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/LICENSE +0 -0
  12. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/README.md +0 -0
  13. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/README_MCP.md +0 -0
  14. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/claude_desktop_config.json +0 -0
  15. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules/__init__.py +0 -0
  16. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules_enhanced.egg-info/SOURCES.txt +0 -0
  17. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules_enhanced.egg-info/dependency_links.txt +0 -0
  18. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules_enhanced.egg-info/requires.txt +0 -0
  19. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyDMNrules_enhanced.egg-info/top_level.txt +0 -0
  20. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pydmnrules_mcp/server.py +0 -0
  21. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/pyproject.toml +0 -0
  22. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/setup.cfg +0 -0
  23. {pydmnrules_enhanced-1.5.0 → pydmnrules_enhanced-1.6.0}/tests/test_pyDMNrules.py +0 -0
@@ -20,3 +20,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
22
 
23
+
@@ -6,3 +6,4 @@ include requirements_mcp.txt
6
6
  include claude_desktop_config.json
7
7
  recursive-include pydmnrules_mcp *.py
8
8
 
9
+
@@ -397,3 +397,4 @@ pyDMNrules MCP Server는 DMN XML 규칙을 LLM이 쉽게 사용할 수 있도록
397
397
  **엔진**: pyDMNrules
398
398
  **프레임워크**: FastMCP
399
399
 
400
+
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pyDMNrules-enhanced
3
- Version: 1.5.0
4
- Summary: Enhanced pyDMNrules with full decision dependency support (<informationRequirement>)
3
+ Version: 1.6.0
4
+ Summary: Enhanced pyDMNrules with full DRD support (informationRequirement/requiredInput auto-mapping)
5
5
  Home-page: https://github.com/uengine/pyDMNrules
6
6
  Author: Russell McDonell (Enhanced by uengine)
7
7
  Author-email: russell.mcdonell@c-cost.com
@@ -16,16 +16,6 @@ Requires-Dist: datetime
16
16
  Requires-Dist: pySFeel
17
17
  Requires-Dist: openpyxl
18
18
  Requires-Dist: pandas
19
- Dynamic: author
20
- Dynamic: author-email
21
- Dynamic: classifier
22
- Dynamic: description
23
- Dynamic: description-content-type
24
- Dynamic: home-page
25
- Dynamic: license-file
26
- Dynamic: requires-dist
27
- Dynamic: requires-python
28
- Dynamic: summary
29
19
 
30
20
  # pyDMNrules
31
21
  An implementation of DMN (Decision Model Notation) in Python, using the [pySFeel](https://pypi.org/project/pySFeel/), [openpyxl](https://pypi.org/project/openpyxl/) and [pandas](httpsL//pypi.org/project/pandas) modules.
@@ -275,3 +275,4 @@ Claude가 자동으로:
275
275
  3. 로그 확인
276
276
  4. 의존성 재설치: `pip install -r requirements_mcp.txt --force-reinstall`
277
277
 
278
+
@@ -44,6 +44,10 @@ class DMN():
44
44
  self.isLoaded = False
45
45
  self.testIsLoaded = False
46
46
  self.decisionDependencies = {} # Decision ID -> list of required decision IDs
47
+ self.decisionIdToTables = {} # Decision ID -> list of table names
48
+ self.decisionTableToId = {} # Table name -> Decision ID
49
+ self.decisionRequiredInputs = {} # Decision ID -> list of required inputData IDs
50
+ self.inputDataIdToVariable = {} # InputData ID -> variable name
47
51
  self.errors = []
48
52
  self.DMNfunctions = [
49
53
  'date(', 'time(', 'date and time(',
@@ -2341,6 +2345,10 @@ class DMN():
2341
2345
  self.decisionName = ''
2342
2346
  self.decisions = []
2343
2347
  self.decisionDependencies = {} # Decision ID -> list of required decision IDs
2348
+ self.decisionIdToTables = {} # Decision ID -> list of table names
2349
+ self.decisionTableToId = {} # Table name -> Decision ID
2350
+ self.decisionRequiredInputs = {} # Decision ID -> list of required inputData IDs
2351
+ self.inputDataIdToVariable = {} # InputData ID -> variable name
2344
2352
  self.decisionHeading = []
2345
2353
  self.otherDecisions = []
2346
2354
  self.decisionTables = {}
@@ -2936,18 +2944,29 @@ class DMN():
2936
2944
  self.decisionHeading = [0, 'Decisions', 'Execute Decision Tables']
2937
2945
  theseTables = list(self.decisionTables)
2938
2946
 
2939
- # Build a mapping from decision IDs to table names
2940
- idToTable = {}
2941
- tableToId = {}
2942
- if hasattr(self, 'decisionDependencies') and self.decisionDependencies:
2943
- for dec_id in self.decisionDependencies:
2944
- # Try to match decision ID with table name
2945
- for table in self.decisionTables:
2946
- # Match by ID or by name similarity
2947
- if dec_id == table or dec_id.lower().replace('_', ' ') == table.lower():
2948
- idToTable[dec_id] = table
2949
- tableToId[table] = dec_id
2950
- break
2947
+ # Use the prebuilt mapping from decision IDs to table names
2948
+ # This mapping was created in useXML() from XMLdecisions
2949
+ if hasattr(self, 'decisionTableToId') and self.decisionTableToId:
2950
+ tableToId = self.decisionTableToId
2951
+ # Build reverse mapping: for each decision ID, get all its tables (first table only for simplicity)
2952
+ idToTable = {}
2953
+ if hasattr(self, 'decisionIdToTables') and self.decisionIdToTables:
2954
+ for dec_id, tables in self.decisionIdToTables.items():
2955
+ if tables:
2956
+ idToTable[dec_id] = tables[0] # Use first table as representative
2957
+ else:
2958
+ # Fallback to old matching logic if mappings not available
2959
+ idToTable = {}
2960
+ tableToId = {}
2961
+ if hasattr(self, 'decisionDependencies') and self.decisionDependencies:
2962
+ for dec_id in self.decisionDependencies:
2963
+ # Try to match decision ID with table name
2964
+ for table in self.decisionTables:
2965
+ # Match by ID or by name similarity
2966
+ if dec_id == table or dec_id.lower().replace('_', ' ') == table.lower():
2967
+ idToTable[dec_id] = table
2968
+ tableToId[table] = dec_id
2969
+ break
2951
2970
 
2952
2971
  while len(theseTables) > 0:
2953
2972
  for i in range(len(theseTables)): # Check every table looking for tables with no dependencies
@@ -3080,6 +3099,10 @@ class DMN():
3080
3099
  self.decisionHeading = [0, 'Decisions', 'Execute Decision Tables'] # There are no input tests and there may be no annotations
3081
3100
  self.decisions = []
3082
3101
  self.decisionDependencies = {} # Decision ID -> list of required decision IDs
3102
+ self.decisionIdToTables = {} # Decision ID -> list of table names
3103
+ self.decisionTableToId = {} # Table name -> Decision ID
3104
+ self.decisionRequiredInputs = {} # Decision ID -> list of required inputData IDs
3105
+ self.inputDataIdToVariable = {} # InputData ID -> variable name
3083
3106
  self.otherDecisions = []
3084
3107
  self.decisionTables = {}
3085
3108
 
@@ -3093,7 +3116,9 @@ class DMN():
3093
3116
  thisName = itemDefinition.get('name')
3094
3117
  XMLitemDefinitions[thisName] = itemDefinition
3095
3118
  XMLvariables = {}
3119
+ XMLinputDataMap = {} # Map inputData ID -> variable name
3096
3120
  for inputData in root.iter(DMN + 'inputData'):
3121
+ inputDataId = inputData.get('id')
3097
3122
  thisVariable = inputData.find('variable', namespaces=ns)
3098
3123
  if thisVariable is None:
3099
3124
  continue
@@ -3103,6 +3128,11 @@ class DMN():
3103
3128
  status['errors'] = self.errors
3104
3129
  return status
3105
3130
  variable = thisVariable.get('name')
3131
+
3132
+ # Store mapping from inputData ID to variable name
3133
+ if inputDataId:
3134
+ XMLinputDataMap[inputDataId] = variable
3135
+
3106
3136
  if 'typeRef' not in thisVariable.keys():
3107
3137
  continue
3108
3138
  typeRef = thisVariable.get('typeRef')
@@ -3190,17 +3220,23 @@ class DMN():
3190
3220
  XMLvariables[variable] = typeRef
3191
3221
  outputs = thisTable.findall('output', namespaces=ns)
3192
3222
  for thisOutput in outputs:
3193
- if 'label' in thisOutput.keys():
3223
+ # Prefer 'name' attribute over 'label' for output variable name (DMN standard)
3224
+ if 'name' in thisOutput.keys():
3225
+ variable = thisOutput.get('name')
3226
+ elif 'label' in thisOutput.keys():
3194
3227
  variable = thisOutput.get('label')
3195
- if (thisTable in XMLdecisionBindings) and (variable in XMLdecisionBindings[thisTable]):
3196
- variable = XMLdecisionBindings[thisTable][variable]
3197
- elif (decisionName in XMLdecisionVariables) and (len(XMLdecisionVariables[decisionName]) == 1) and (len(outputs) == 1):
3198
- variable = XMLdecisionVariables[decisionName][0]
3199
- if variable not in XMLvariables:
3200
- outputExpression = thisOutput.find('inputExpression', namespaces=ns)
3201
- if (outputExpression is not None) and ('typeRef' in outputExpression.keys()):
3202
- typeRef = outputExpression.get('typeRef')
3203
- XMLvariables[variable] = typeRef
3228
+ else:
3229
+ continue
3230
+
3231
+ if (thisTable in XMLdecisionBindings) and (variable in XMLdecisionBindings[thisTable]):
3232
+ variable = XMLdecisionBindings[thisTable][variable]
3233
+ elif (decisionName in XMLdecisionVariables) and (len(XMLdecisionVariables[decisionName]) == 1) and (len(outputs) == 1):
3234
+ variable = XMLdecisionVariables[decisionName][0]
3235
+ if variable not in XMLvariables:
3236
+ outputExpression = thisOutput.find('inputExpression', namespaces=ns)
3237
+ if (outputExpression is not None) and ('typeRef' in outputExpression.keys()):
3238
+ typeRef = outputExpression.get('typeRef')
3239
+ XMLvariables[variable] = typeRef
3204
3240
 
3205
3241
  for variable in XMLvariables:
3206
3242
  typeRef = XMLvariables[variable]
@@ -3317,6 +3353,7 @@ class DMN():
3317
3353
  XMLdecisions[-1]['tables'] = []
3318
3354
  XMLdecisions[-1]['annotations'] = {}
3319
3355
  XMLdecisions[-1]['required'] = []
3356
+ XMLdecisions[-1]['requiredInputs'] = [] # Track required inputData
3320
3357
  XMLdecisions[-1]['done'] = False
3321
3358
  for infoRequired in decision.findall('informationRequirement', namespaces=ns):
3322
3359
  requiredDecision = infoRequired.find('requiredDecision', namespaces=ns)
@@ -3334,6 +3371,23 @@ class DMN():
3334
3371
  status = {}
3335
3372
  status['errors'] = self.errors
3336
3373
  return status
3374
+
3375
+ # Also handle requiredInput (inputData -> decision)
3376
+ requiredInput = infoRequired.find('requiredInput', namespaces=ns)
3377
+ if requiredInput is not None:
3378
+ if 'href' not in requiredInput.keys():
3379
+ self.errors.append('Invalid XML')
3380
+ status = {}
3381
+ status['errors'] = self.errors
3382
+ return status
3383
+ inputRef = requiredInput.get('href')
3384
+ if inputRef[0] == '#': # Internal reference
3385
+ XMLdecisions[-1]['requiredInputs'].append(inputRef[1:]) # The 'id' of the required inputData
3386
+ else:
3387
+ self.errors.append('Invalid XML')
3388
+ status = {}
3389
+ status['errors'] = self.errors
3390
+ return status
3337
3391
 
3338
3392
  # There can be multiple decisionTable for each decision
3339
3393
  theseTables = list(decision.findall('decisionTable', namespaces=ns))
@@ -3703,8 +3757,11 @@ class DMN():
3703
3757
  status = {}
3704
3758
  status['errors'] = self.errors
3705
3759
  return status
3706
- variable = text.text
3707
- if 'label' in inputVariable.keys():
3760
+ # Use inputExpression text as variable name (DMN standard)
3761
+ # The text is the FEEL expression that references the actual variable
3762
+ variable = text.text if text.text else ''
3763
+ # Only use label as fallback if text is empty
3764
+ if variable == '' and 'label' in inputVariable.keys():
3708
3765
  variable = inputVariable.get('label')
3709
3766
  if (table in XMLdecisionBindings) and (variable in XMLdecisionBindings[table]):
3710
3767
  variable = XMLdecisionBindings[table][variable]
@@ -3845,16 +3902,17 @@ class DMN():
3845
3902
  # Outputs should have a label (for linkage to inputs) and can have outputValues
3846
3903
  outputs = []
3847
3904
  for outputVariable in XMLdecisionTable.findall('output', namespaces=ns):
3848
- if 'label' in outputVariable.keys():
3849
- if outputLabel != '':
3850
- variable = outputLabel
3851
- else:
3852
- variable = outputVariable.get('label')
3853
- elif 'name' in outputVariable.keys():
3905
+ # Prefer 'name' attribute over 'label' for output variable name (DMN standard)
3906
+ if 'name' in outputVariable.keys():
3854
3907
  if outputLabel != '':
3855
3908
  variable = outputLabel + '.' + outputVariable.get('name')
3856
3909
  else:
3857
3910
  variable = outputVariable.get('name')
3911
+ elif 'label' in outputVariable.keys():
3912
+ if outputLabel != '':
3913
+ variable = outputLabel
3914
+ else:
3915
+ variable = outputVariable.get('label')
3858
3916
  elif outputLabel != '':
3859
3917
  variable = outputLabel
3860
3918
  outputLabel = ''
@@ -4100,11 +4158,23 @@ class DMN():
4100
4158
 
4101
4159
  # Save decision dependencies from XMLdecisions
4102
4160
  self.decisionDependencies = {}
4161
+ self.decisionIdToTables = {} # Map decision ID to its table names
4162
+ self.decisionTableToId = {} # Map table name to its decision ID
4163
+ self.decisionRequiredInputs = {} # Map decision ID to required inputData IDs
4164
+ self.inputDataIdToVariable = XMLinputDataMap # Map inputData ID to variable name
4165
+
4103
4166
  for xmlDec in XMLdecisions:
4104
4167
  dec_id = xmlDec.get('id')
4105
4168
  required_ids = xmlDec.get('required', [])
4169
+ required_inputs = xmlDec.get('requiredInputs', [])
4170
+ tables = xmlDec.get('tables', [])
4106
4171
  if dec_id:
4107
4172
  self.decisionDependencies[dec_id] = required_ids
4173
+ self.decisionRequiredInputs[dec_id] = required_inputs
4174
+ self.decisionIdToTables[dec_id] = tables
4175
+ # Create reverse mapping for each table
4176
+ for table in tables:
4177
+ self.decisionTableToId[table] = dec_id
4108
4178
 
4109
4179
  # Now build a decision order
4110
4180
  theseTables = self.buildDecision(theseInputs, theseOutputs)
@@ -5215,6 +5285,10 @@ class DMN():
5215
5285
  self.errors = []
5216
5286
  return (status, {})
5217
5287
  self.initGlossary()
5288
+
5289
+ # Store original input data keys for later requiredInput mapping
5290
+ original_input_keys = set(data.keys())
5291
+
5218
5292
  validData = True
5219
5293
  for variable in data:
5220
5294
  value = data[variable]
@@ -5271,6 +5345,53 @@ class DMN():
5271
5345
  self.errors = []
5272
5346
  return (status, {})
5273
5347
 
5348
+ # Handle requiredInput automatic mapping (inputData variable -> inputExpression text)
5349
+ # For each decision table, check if it has requiredInputs and map inputData values to inputExpression variables
5350
+ if hasattr(self, 'decisionTableToId') and hasattr(self, 'decisionRequiredInputs') and hasattr(self, 'inputDataIdToVariable'):
5351
+ for table_name in self.decisionTables:
5352
+ # Get the decision ID for this table
5353
+ if table_name in self.decisionTableToId:
5354
+ dec_id = self.decisionTableToId[table_name]
5355
+ # Get the required input IDs for this decision
5356
+ if dec_id in self.decisionRequiredInputs:
5357
+ required_input_ids = self.decisionRequiredInputs[dec_id]
5358
+ # Get the inputColumns for this table
5359
+ input_columns = self.decisionTables[table_name].get('inputColumns', [])
5360
+
5361
+ # Map each required inputData to corresponding inputExpression
5362
+ for i, input_data_id in enumerate(required_input_ids):
5363
+ if input_data_id in self.inputDataIdToVariable and i < len(input_columns):
5364
+ input_var_name = self.inputDataIdToVariable[input_data_id]
5365
+ input_expr_name = input_columns[i].get('name')
5366
+
5367
+ # If inputData variable exists in glossary and was provided
5368
+ if input_var_name in self.glossary and input_expr_name and input_var_name in original_input_keys:
5369
+ input_item = self.glossary[input_var_name]['item']
5370
+ # Get the value
5371
+ (failed, input_value) = self.sfeel('{}'.format(input_item))
5372
+ if not failed and input_value is not None:
5373
+ # Create a glossary entry for the inputExpression name if it doesn't exist
5374
+ if input_expr_name not in self.glossary:
5375
+ attribute = re.sub(self.badFEELchars, '', input_expr_name.replace(' ', '_'))
5376
+ expr_item = 'Data.' + attribute
5377
+ if expr_item not in self.glossaryItems:
5378
+ self.glossary[input_expr_name] = {}
5379
+ self.glossary[input_expr_name]['item'] = expr_item
5380
+ self.glossary[input_expr_name]['concept'] = 'Data'
5381
+ self.glossaryItems[expr_item] = input_expr_name
5382
+ thisLen = len(input_expr_name)
5383
+ if thisLen not in self.glossaryLen:
5384
+ self.glossaryLen[thisLen] = []
5385
+ self.glossaryLen[thisLen].append(input_expr_name)
5386
+ self.glossary[input_expr_name]['annotations'] = []
5387
+
5388
+ # Store the value under the inputExpression name
5389
+ if input_expr_name in self.glossary:
5390
+ expr_item = self.glossary[input_expr_name]['item']
5391
+ sFeelValue = self.value2sfeel(input_value)
5392
+ if sFeelValue is not None:
5393
+ (failed, retVal) = self.sfeel('{} <- {}'.format(expr_item, sFeelValue))
5394
+
5274
5395
  # Initialize the status so we can detect circular references
5275
5396
  for table in self.decisionTables:
5276
5397
  self.decisionTables[table]['status'] = 'idle'
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: pyDMNrules-enhanced
3
- Version: 1.5.0
4
- Summary: Enhanced pyDMNrules with full decision dependency support (<informationRequirement>)
3
+ Version: 1.6.0
4
+ Summary: Enhanced pyDMNrules with full DRD support (informationRequirement/requiredInput auto-mapping)
5
5
  Home-page: https://github.com/uengine/pyDMNrules
6
6
  Author: Russell McDonell (Enhanced by uengine)
7
7
  Author-email: russell.mcdonell@c-cost.com
@@ -16,16 +16,6 @@ Requires-Dist: datetime
16
16
  Requires-Dist: pySFeel
17
17
  Requires-Dist: openpyxl
18
18
  Requires-Dist: pandas
19
- Dynamic: author
20
- Dynamic: author-email
21
- Dynamic: classifier
22
- Dynamic: description
23
- Dynamic: description-content-type
24
- Dynamic: home-page
25
- Dynamic: license-file
26
- Dynamic: requires-dist
27
- Dynamic: requires-python
28
- Dynamic: summary
29
19
 
30
20
  # pyDMNrules
31
21
  An implementation of DMN (Decision Model Notation) in Python, using the [pySFeel](https://pypi.org/project/pySFeel/), [openpyxl](https://pypi.org/project/openpyxl/) and [pandas](httpsL//pypi.org/project/pandas) modules.
@@ -12,3 +12,4 @@ pydmnrules-enhanced>=1.5.0
12
12
  pytest>=7.4.0
13
13
  pytest-asyncio>=0.21.0
14
14
 
15
+
@@ -5,10 +5,10 @@ with open('README.md', 'r') as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name='pyDMNrules-enhanced',
8
- version='1.5.0',
8
+ version='1.6.0',
9
9
  author='Russell McDonell (Enhanced by uengine)',
10
10
  author_email='russell.mcdonell@c-cost.com',
11
- description='Enhanced pyDMNrules with full decision dependency support (<informationRequirement>)',
11
+ description='Enhanced pyDMNrules with full DRD support (informationRequirement/requiredInput auto-mapping)',
12
12
  long_description=long_description,
13
13
  long_description_content_type='text/markdown',
14
14
  url='https://github.com/uengine/pyDMNrules',