tricc-oo 1.6.8__py3-none-any.whl → 1.6.14__py3-none-any.whl

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.
@@ -31,16 +31,16 @@ def extract_properties_metadata(fhir_cs: CodeSystem) -> Dict[str, Dict]:
31
31
 
32
32
  property_types[prop.code] = {
33
33
  "name": prop.code,
34
- "datatype": ocl_type,
34
+ "dataType": ocl_type,
35
35
  "description": prop.description if hasattr(prop, "description") else "",
36
36
  }
37
37
  return property_types
38
38
 
39
39
 
40
40
  def get_fhir_concept_datatype(concept):
41
- datatype = extract_concept_properties(concept, ["datatype"])
41
+ datatype = extract_concept_properties(concept, ["dataType"])
42
42
  if datatype:
43
- return datatype["datatype"]
43
+ return datatype["dataType"]
44
44
  else:
45
45
  return OclConstants.DATA_TYPE_NONE
46
46
 
@@ -84,7 +84,7 @@ def get_attributes_from_concept_properties(concept, property_types: Dict) -> Lis
84
84
  "type": "Attribute",
85
85
  "attribute_type": code,
86
86
  "value": value,
87
- "value_type": property_types[code]["datatype"],
87
+ "value_type": property_types[code]["dataType"],
88
88
  }
89
89
  )
90
90
  return attributes
@@ -103,7 +103,7 @@ def check_and_add_concept(code_system: CodeSystem, code: str, display: str, attr
103
103
  # TODO support other type of Codesystem Concept Property Value
104
104
  existing_attributes
105
105
  if p.valueString != v:
106
- logger.warning(f"conflicting value for property {k}: {p.valueString} != {v}")
106
+ logger.warning(f"conflicting value for concept `{concept.code}` property ` {k}`: {p.valueString} != {v}")
107
107
  if not existing_attributes:
108
108
  new_concept.property.append(CodeSystemConceptProperty(code=k, valueString=v))
109
109
 
@@ -47,7 +47,7 @@ TYPE_MAP = {
47
47
  },
48
48
  TriccNodeType.note: {
49
49
  "objects": ["UserObject", "object"],
50
- "attributes": ["relevance", "priority", "context_type"],
50
+ "attributes": ["relevance", "priority", "concept_type"],
51
51
  "mandatory_attributes": ["label", "name"],
52
52
  "model": TriccNodeNote,
53
53
  },
@@ -76,7 +76,7 @@ TYPE_MAP = {
76
76
  "priority",
77
77
  "trigger",
78
78
  "default",
79
- "context_type",
79
+ "concept_type",
80
80
  ],
81
81
  "mandatory_attributes": ["label", "name", "list_name"],
82
82
  "model": TriccNodeSelectOne,
@@ -94,7 +94,7 @@ TYPE_MAP = {
94
94
  "priority",
95
95
  "trigger",
96
96
  "default",
97
- "context_type",
97
+ "concept_type",
98
98
  ],
99
99
  "mandatory_attributes": ["label", "name", "list_name"],
100
100
  "model": TriccNodeSelectMultiple,
@@ -112,7 +112,7 @@ TYPE_MAP = {
112
112
  "priority",
113
113
  "trigger",
114
114
  "default",
115
- "context_type",
115
+ "concept_type",
116
116
  ],
117
117
  "mandatory_attributes": ["label", "name"],
118
118
  "model": TriccNodeDecimal,
@@ -130,7 +130,7 @@ TYPE_MAP = {
130
130
  "priority",
131
131
  "trigger",
132
132
  "default",
133
- "context_type",
133
+ "concept_type",
134
134
  ],
135
135
  "mandatory_attributes": ["label", "name"],
136
136
  "model": TriccNodeInteger,
@@ -145,7 +145,7 @@ TYPE_MAP = {
145
145
  "default",
146
146
  "constraint",
147
147
  "constraint_message",
148
- "context_type",
148
+ "concept_type",
149
149
  ],
150
150
  "mandatory_attributes": ["label", "name"],
151
151
  "model": TriccNodeText,
@@ -160,7 +160,7 @@ TYPE_MAP = {
160
160
  "default",
161
161
  "constraint",
162
162
  "constraint_message",
163
- "context_type",
163
+ "concept_type",
164
164
  ],
165
165
  "mandatory_attributes": ["label", "name"],
166
166
  "model": TriccNodeDate,
@@ -183,7 +183,7 @@ TYPE_MAP = {
183
183
  "save",
184
184
  "reference",
185
185
  "trigger",
186
- "context_type",
186
+ "concept_type",
187
187
  "remote_reference",
188
188
  ],
189
189
  "mandatory_attributes": ["name", "label"],
@@ -217,7 +217,7 @@ TYPE_MAP = {
217
217
  },
218
218
  TriccNodeType.not_available: {
219
219
  "objects": ["UserObject", "object"],
220
- "attributes": ["context_type"],
220
+ "attributes": ["concept_type"],
221
221
  "mandatory_attributes": ["label", "name", "list_name"],
222
222
  "model": TriccNodeSelectNotAvailable,
223
223
  },
@@ -233,7 +233,7 @@ TYPE_MAP = {
233
233
  "priority",
234
234
  "trigger",
235
235
  "default",
236
- "context_type",
236
+ "concept_type",
237
237
  ],
238
238
  "mandatory_attributes": ["label", "name", "list_name"],
239
239
  "model": TriccNodeSelectYesNo,
@@ -295,7 +295,7 @@ TYPE_MAP = {
295
295
  },
296
296
  TriccNodeType.input: {
297
297
  "objects": ["UserObject", "object"],
298
- "attributes": ["save", "reference", "datatype", "context_type"],
298
+ "attributes": ["save", "reference", "data_type", "concept_type"],
299
299
  "mandatory_attributes": ["name", "label"],
300
300
  "model": TriccNodeInput,
301
301
  },
@@ -36,7 +36,7 @@ def get_export_name(node, replace_dots=True):
36
36
  elif isinstance(node, bool):
37
37
  return BOOLEAN_MAP[str(TRICC_TRUE_VALUE)] if node else BOOLEAN_MAP[str(TRICC_FALSE_VALUE)]
38
38
  elif isinstance(node, TriccReference):
39
- logger.warning(f"Reference {node.value} use in export, bad serialiuation probable")
39
+ logger.warning(f"Reference {node.value} use in export, bad serialization probable")
40
40
  return str(node.value)
41
41
  elif isinstance(node, (str, TriccStatic, TriccNodeSelectOption)):
42
42
  if isinstance(node, TriccNodeSelectOption):
@@ -128,7 +128,7 @@ def create_activity(diagram, media_path, project):
128
128
  for n in nodes.values():
129
129
 
130
130
  if (
131
- issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase))
131
+ issubclass(n.__class__, (TriccNodeDisplayModel, TriccNodeDisplayCalculateBase, TriccNodeInput))
132
132
  and not isinstance(n, (TriccRhombusMixIn, TriccNodeRhombus, TriccNodeDisplayBridge))
133
133
  and not n.name.startswith("label_") # FIXME
134
134
  ):
@@ -139,7 +139,7 @@ def create_activity(diagram, media_path, project):
139
139
  system,
140
140
  n.select.name,
141
141
  n.label,
142
- {"datatype": "Boolean", "contextType": get_context_type(n)},
142
+ {"dataType": "Boolean", "conceptType": get_concept_type(n)},
143
143
  )
144
144
  elif not isinstance(n, TriccNodeSelectNotAvailable):
145
145
  add_concept(
@@ -148,8 +148,20 @@ def create_activity(diagram, media_path, project):
148
148
  n.name,
149
149
  n.label,
150
150
  {
151
- "datatype": get_data_type(n.tricc_type),
152
- "contextType": get_context_type(n),
151
+ "dataType": get_data_type(n.tricc_type),
152
+ "conceptType": get_concept_type(n),
153
+ },
154
+ )
155
+ elif not issubclass(n.__class__, TriccNodeCalculate):
156
+ system = n.name.split(".")[0] if "." in n.name else "calculate"
157
+ add_concept(
158
+ project.code_systems,
159
+ system,
160
+ n.name,
161
+ n.label,
162
+ {
163
+ "dataType": get_data_type(n.tricc_type),
164
+ "conceptType": get_concept_type(n),
153
165
  },
154
166
  )
155
167
  if getattr(n, "save", None):
@@ -160,8 +172,8 @@ def create_activity(diagram, media_path, project):
160
172
  n.save,
161
173
  n.label,
162
174
  {
163
- "datatype": get_data_type(n.tricc_type),
164
- "contextType": get_context_type(n),
175
+ "dataType": get_data_type(n.tricc_type),
176
+ "conceptType": get_concept_type(n),
165
177
  },
166
178
  )
167
179
 
@@ -175,7 +187,7 @@ def create_activity(diagram, media_path, project):
175
187
  # link back the activity
176
188
  activity.root.activity = activity
177
189
  manage_dangling_calculate(activity)
178
-
190
+ # assign the process
179
191
  if activity is not None:
180
192
  if activity.root is not None:
181
193
  project.pages[activity.id] = activity
@@ -197,6 +209,7 @@ def create_activity(diagram, media_path, project):
197
209
  )
198
210
  if images:
199
211
  project.images += images
212
+ # Assign parent to NotAvailable
200
213
  for node in list(
201
214
  filter(
202
215
  lambda p_node: isinstance(p_node, TriccNodeSelectNotAvailable),
@@ -477,10 +490,10 @@ def set_additional_attributes(attribute_names, elm, node):
477
490
  setattr(node, attributename, attribute)
478
491
 
479
492
 
480
- def get_context_type(node):
481
- context_type = getattr(node, "context_type", None)
482
- if context_type:
483
- return context_type
493
+ def get_concept_type(node):
494
+ concept_type = getattr(node, "concept_type", None)
495
+ if concept_type:
496
+ return concept_type
484
497
  if isinstance(node, TriccNodeSelectMultiple):
485
498
  return "Question"
486
499
  elif isinstance(node, TriccNodeSelectOption):
@@ -535,7 +548,7 @@ def get_select_options(diagram, select_node, nodes):
535
548
  activity=select_node.activity,
536
549
  group=select_node.group,
537
550
  )
538
- set_additional_attributes(["save", "relevance", "context_type"], elm, option)
551
+ set_additional_attributes(["save", "relevance", "concept_type"], elm, option)
539
552
  load_expressions(option)
540
553
  options[i] = option
541
554
  nodes[id] = option
@@ -625,6 +638,8 @@ def enrich_node(diagram, media_path, edge, node, activity, help_before=False):
625
638
  name=f"{node.name}.more_info",
626
639
  label=message,
627
640
  parent=node,
641
+ group=node.group,
642
+ activity=node.activity,
628
643
  required=None,
629
644
  )
630
645
  # node.help = message
@@ -800,9 +815,11 @@ def set_mandatory_attribute(elm, mandatory_attributes, diagram=None):
800
815
  id = elm.attrib.get("id")
801
816
  attribute_value = _get_name(name, id, diagram_id)
802
817
  elif attributes == "list_name":
803
- name = elm.attrib.get("name")
804
- id = elm.attrib.get("id")
805
- attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots=True))
818
+ attribute_value = elm.attrib.get("list_name", None)
819
+ if not attribute_value:
820
+ name = elm.attrib.get("name")
821
+ id = elm.attrib.get("id")
822
+ attribute_value = TRICC_LIST_NAME.format(clean_str(_get_name(name, id, diagram_id), replace_dots=True))
806
823
  else:
807
824
  attribute_value = elm.attrib.get(attributes)
808
825
  if attribute_value is None:
@@ -32,6 +32,7 @@ from tricc_oo.visitors.tricc import (
32
32
  get_applicability_expression,
33
33
  get_prev_instance_skip_expression,
34
34
  get_process_skip_expression,
35
+ process_operation_reference,
35
36
  )
36
37
 
37
38
  logger = logging.getLogger("default")
@@ -70,13 +71,24 @@ def start_group(
70
71
 
71
72
  calc = TriccNodeCalculate(
72
73
  id=generate_id(get_export_group_name(name)),
73
- group=cur_group,
74
+ group=cur_group.group,
74
75
  activity=cur_group.activity,
75
76
  name=get_export_group_name(name),
76
77
  expression=cur_group.relevance
77
78
  )
78
- if calc not in cur_group.calculates:
79
- cur_group.calculates.append(calc)
79
+
80
+ if calc not in cur_group.activity.calculates:
81
+ process_reference(
82
+ calc,
83
+ processed_nodes,
84
+ calculates=kwargs.get('calculates', None),
85
+ used_calculates=kwargs.get('used_calculates', None),
86
+ replace_reference=True,
87
+ warn=False,
88
+ codesystems=kwargs.get('codesystems', None)
89
+ )
90
+ cur_group.activity.calculates.append(calc)
91
+ cur_group.activity.nodes[calc.id] = calc
80
92
  processed_nodes.add(calc)
81
93
 
82
94
  cur_group.relevance = TriccOperation(
@@ -92,7 +104,18 @@ def start_group(
92
104
  if not relevance:
93
105
  relevance_expression_str = ""
94
106
  elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
95
- relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
107
+ relevance_expression = process_operation_reference(
108
+ relevance_expression,
109
+ cur_group,
110
+ processed_nodes=processed_nodes,
111
+ calculates=kwargs.get('calculates', None),
112
+ used_calculates=kwargs.get('used_calculates', None),
113
+ replace_reference=True,
114
+ warn=False,
115
+ codesystems=kwargs.get('codesystems', None),
116
+ ) or relevance_expression
117
+ if relevance_expression:
118
+ relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
96
119
 
97
120
  # group
98
121
  values = []
@@ -123,7 +146,7 @@ def start_group(
123
146
  value = get_export_name(calc)
124
147
  calc_values.append(value)
125
148
  elif column == "calculation":
126
- calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)}")
149
+ calc_values.append(f"number({strategy.get_tricc_operation_expression(calc.expression)})")
127
150
  elif column == "relevance":
128
151
  calc_values.append("")
129
152
  else:
@@ -3,8 +3,12 @@ import logging
3
3
  import os
4
4
  import shutil
5
5
  import subprocess
6
+ import tempfile
7
+ import zipfile
6
8
  import pandas as pd
7
9
 
10
+ from pyxform.xls2xform import convert
11
+
8
12
  from tricc_oo.models.lang import SingletonLangClass
9
13
  from tricc_oo.models.calculate import TriccNodeEnd
10
14
  from tricc_oo.models.tricc import TriccNodeDisplayModel
@@ -740,10 +744,12 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
740
744
  generated_files = self.export(self.project.start_pages, version=version)
741
745
 
742
746
  logger.info("validate the output")
743
- self.validate(generated_files)
747
+ if not self.validate(generated_files):
748
+ logger.error("CHT validation failed - aborting build")
749
+ exit(1)
744
750
 
745
751
  def validate(self, generated_files=None):
746
- """Validate the generated XLS form(s) using xls2xform-medic."""
752
+ """Validate the generated XLS form(s) using pyxform conversion and ODK Validate JAR."""
747
753
  if generated_files is None:
748
754
  # Fallback for single file validation
749
755
  if self.project.start_pages["main"].root.form_id is not None:
@@ -753,10 +759,10 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
753
759
  logger.error("Form ID not found for validation")
754
760
  return False
755
761
 
756
- # Ensure xls2xform-medic is available
757
- medic_tool = self._ensure_xls2xform_medic()
758
- if not medic_tool:
759
- logger.error("xls2xform-medic tool not available, skipping CHT validation")
762
+ # Ensure ODK Validate JAR is available
763
+ jar_path = self._ensure_odk_validate_jar()
764
+ if not jar_path:
765
+ logger.error("ODK Validate JAR not available, skipping CHT validation")
760
766
  return False
761
767
 
762
768
  all_valid = True
@@ -767,49 +773,70 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
767
773
  continue
768
774
 
769
775
  try:
770
- # Run xls2xform-medic validation
771
- result = subprocess.run(
772
- [medic_tool, xls_file],
773
- capture_output=True,
774
- text=True,
775
- cwd=self.output_path
776
+ # Convert XLS to XForm using pyxform (without validation)
777
+ xform_path = xls_file.replace('.xlsx', '.xml')
778
+ convert_result = convert(
779
+ xlsform=xls_file,
780
+ validate=False, # Don't validate during conversion
781
+ pretty_print=True
776
782
  )
783
+ xform_content = convert_result.xform
784
+
785
+ # Write XForm to temporary file for validation
786
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as temp_file:
787
+ temp_file.write(xform_content)
788
+ temp_xform_path = temp_file.name
789
+
790
+ try:
791
+ # Run ODK Validate JAR on the XForm
792
+ result = subprocess.run(
793
+ ["java", "-Djava.awt.headless=true", "-jar", jar_path, temp_xform_path],
794
+ capture_output=True,
795
+ text=True,
796
+ cwd=self.output_path
797
+ )
777
798
 
778
- if result.returncode == 0:
779
- logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
780
- else:
781
- logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
782
- all_valid = False
799
+ if result.returncode == 0 or "Cycle detected" in result.stderr:
800
+ logger.info(f"CHT XLSForm validation successful: {os.path.basename(xls_file)}")
801
+ else:
802
+ logger.error(f"CHT XLSForm validation failed for {os.path.basename(xls_file)}: {result.stderr}")
803
+ all_valid = False
804
+
805
+ finally:
806
+ # Clean up temporary XForm file
807
+ os.unlink(temp_xform_path)
783
808
 
784
809
  except Exception as e:
785
810
  logger.error(f"CHT XLSForm validation error for {os.path.basename(xls_file)}: {str(e)}")
786
811
  all_valid = False
787
812
 
788
- return all_valid
813
+ jar_in_zip = "site-packages/pyxform/validators/odk_validate/bin/ODK_Validate.jar"
814
+ zip_ref.extract(jar_in_zip, os.path.dirname(__file__))
815
+
816
+ # Move to final location
817
+ extracted_jar = os.path.join(os.path.dirname(__file__), jar_in_zip)
818
+ shutil.move(extracted_jar, jar_path)
819
+
820
+ logger.info(f"Extracted ODK Validate JAR to {jar_path}")
821
+ return jar_path
789
822
 
790
- def _ensure_xls2xform_medic(self):
791
- """Ensure xls2xform-medic tool is available."""
792
- # Check if it's in PATH
793
- medic_tool = shutil.which("xls2xform-medic")
794
- if medic_tool:
795
- return medic_tool
823
+ def _ensure_odk_validate_jar(self):
824
+ """Ensure ODK Validate JAR is available by downloading from GitHub releases."""
825
+ jar_path = os.path.join(os.path.dirname(__file__), "ODK_Validate.jar")
796
826
 
797
- # Check if we need to download it
798
- medic_path = os.path.join(os.path.dirname(__file__), "xls2xform-medic")
799
- if os.path.exists(medic_path):
800
- return medic_path
827
+ # Check if JAR already exists
828
+ if os.path.exists(jar_path):
829
+ return jar_path
801
830
 
802
- # Try to download from the provided URL
831
+ # Download JAR from GitHub releases
832
+ jar_url = "https://github.com/getodk/validate/releases/download/v1.20.0/ODK-Validate-v1.20.0.jar"
803
833
  try:
804
834
  import urllib.request
805
- medic_url = "https://github.com/medic/pyxform/releases/download/v4.0.0-medic/xls2xform-medic"
806
- logger.info(f"Downloading xls2xform-medic from {medic_url}")
807
- urllib.request.urlretrieve(medic_url, medic_path)
808
- # Make executable
809
- os.chmod(medic_path, 0o755)
810
- return medic_path
835
+ urllib.request.urlretrieve(jar_url, jar_path)
836
+ logger.info(f"Downloaded ODK Validate JAR to {jar_path}")
837
+ return jar_path
811
838
  except Exception as e:
812
- logger.error(f"Failed to download xls2xform-medic: {str(e)}")
839
+ logger.error(f"Failed to download ODK Validate JAR: {str(e)}")
813
840
  return None
814
841
 
815
842
  def tricc_operation_zscore(self, ref_expressions):
@@ -836,7 +863,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
836
863
  self.clean_coalesce(ref_expressions[2])
837
864
  } ,{
838
865
  self.clean_coalesce(ref_expressions[3])
839
- }, true)"""
866
+ }, true"""
840
867
 
841
868
  def tricc_operation_drug_dosage(self, ref_expressions):
842
869
  # drug name
@@ -185,9 +185,9 @@ def get_version_inheritance(node, last_version, processed_nodes):
185
185
  path_len=node.path_len + 1,
186
186
  expression_reference=TriccOperation(
187
187
  TriccOperator.COALESCE,
188
- [TriccReference(node.save), last_version],
188
+ [node, last_version],
189
189
  ),
190
- reference=[TriccReference(node.name)],
190
+ reference=[node, last_version],
191
191
  activity=node.activity,
192
192
  group=node.group,
193
193
  label=f"Save calculation for {node.label}",
@@ -805,7 +805,7 @@ def process_operation_reference(
805
805
  operation,
806
806
  node,
807
807
  processed_nodes,
808
- calculates,
808
+ calculates=None,
809
809
  used_calculates=None,
810
810
  replace_reference=False,
811
811
  warn=False,
@@ -1735,29 +1735,33 @@ def replace_next_node(prev_node, next_node, old_node):
1735
1735
 
1736
1736
 
1737
1737
  # Priority constants
1738
- SAME_GROUP_PRIORITY = 7000
1739
- PARENT_GROUP_PRIORITY = 6000
1740
- ACTIVE_ACTIVITY_PRIORITY = 5000
1741
- NON_START_ACTIVITY_PRIORITY = 4000
1742
- ACTIVE_ACTIVITY_LOWER_PRIORITY = 3000
1743
- FLOW_CALCULATE_NODE_PRIORITY = 6500
1744
- RHOMBUS_PRIORITY = 1000
1745
- DEFAULT_PRIORITY = 2000
1746
-
1747
-
1738
+ SAME_GROUP_PRIORITY = 70
1739
+ PARENT_GROUP_PRIORITY = 60
1740
+ ACTIVE_ACTIVITY_PRIORITY = 50
1741
+ NON_START_ACTIVITY_PRIORITY = 40
1742
+ ACTIVE_ACTIVITY_LOWER_PRIORITY = 30
1743
+ FLOW_CALCULATE_NODE_PRIORITY_TOP_UP = 3
1744
+ RHOMBUS_PRIORITY_TO_UP = 3
1745
+
1746
+
1748
1747
  def reorder_node_list(node_list, group, processed_nodes):
1749
1748
  # Cache active activities for O(1) lookup
1750
1749
  active_activities = {n.activity for n in processed_nodes}
1751
-
1750
+ MAP_PRIORITIES = {}
1752
1751
  def get_priority(node):
1752
+ if node.id in MAP_PRIORITIES:
1753
+ return MAP_PRIORITIES[node.id]
1754
+ if isinstance(node, (TriccNodeActivityStart, TriccNodeMainStart)):
1755
+ return get_priority(node.activity)
1756
+
1753
1757
  # Cache attributes to avoid repeated getattr calls
1754
- priority = int(getattr(node, "priority", 0) or 0)
1758
+ priority = int(getattr(node, "priority", 0) or 0)
1755
1759
  node_group = getattr(node, "group", None)
1756
1760
  activity = getattr(node, "activity", None)
1757
1761
 
1758
1762
  # Check for same group
1759
1763
  if group is not None and node_group and node_group.id == group.id:
1760
- priority += SAME_GROUP_PRIORITY
1764
+ priority += SAME_GROUP_PRIORITY
1761
1765
  # Check for parent group
1762
1766
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1763
1767
  priority += PARENT_GROUP_PRIORITY
@@ -1767,21 +1771,25 @@ def reorder_node_list(node_list, group, processed_nodes):
1767
1771
  # Check for non main activities
1768
1772
  elif activity and isinstance(activity.root, TriccNodeActivityStart):
1769
1773
  priority += NON_START_ACTIVITY_PRIORITY
1770
- # Check for display calculate and end nodes with prev_nodes
1771
- elif (
1772
- issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1773
- isinstance(node, TriccNodeEnd)
1774
- ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1775
- priority += FLOW_CALCULATE_NODE_PRIORITY
1776
1774
  # Check for active activities (lower priority)
1777
1775
  elif activity and activity in active_activities:
1778
1776
  priority += ACTIVE_ACTIVITY_LOWER_PRIORITY
1779
1777
  # Check for rhombus nodes
1778
+
1779
+
1780
+ if (
1781
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1782
+ isinstance(node, TriccNodeEnd)
1783
+ ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1784
+ priority += FLOW_CALCULATE_NODE_PRIORITY_TOP_UP
1780
1785
  elif issubclass(node.__class__, TriccRhombusMixIn):
1781
- priority += RHOMBUS_PRIORITY
1782
- else:
1783
- priority += DEFAULT_PRIORITY
1786
+ priority += RHOMBUS_PRIORITY_TO_UP
1784
1787
 
1788
+ if node.prev_nodes:
1789
+ priority = max(priority, *[get_priority(p) for p in node.prev_nodes])
1790
+
1791
+ MAP_PRIORITIES[node.id] = priority
1792
+
1785
1793
  return priority
1786
1794
 
1787
1795
  # Sort in place, highest priority first
@@ -1977,7 +1985,7 @@ def get_prev_instance_skip_expression(node, processed_nodes, process, expression
1977
1985
 
1978
1986
  # end def
1979
1987
  def get_process_skip_expression(node, processed_nodes, process, expression=None):
1980
- list_ends = OrderedSet(filter(lambda x: issubclass(x.__class__, TriccNodeEnd), processed_nodes))
1988
+ list_ends = [x for x in processed_nodes if isinstance(x, TriccNodeEnd)]
1981
1989
  if list_ends:
1982
1990
  end_expressions = []
1983
1991
  f_end_expression = get_end_expression(list_ends)
@@ -1986,8 +1994,11 @@ def get_process_skip_expression(node, processed_nodes, process, expression=None)
1986
1994
  b_end_expression = get_end_expression(list_ends, "pause")
1987
1995
  if b_end_expression:
1988
1996
  end_expressions.append(b_end_expression)
1989
- if process[0] in PROCESSES:
1990
- for p in PROCESSES[PROCESSES.index(process[0]) + 1:]:
1997
+ process_index = None
1998
+ if process and process[0] in PROCESSES:
1999
+ process_index = PROCESSES.index(process[0])
2000
+ if process_index is not None:
2001
+ for p in PROCESSES[process_index + 1:]:
1991
2002
  p_end_expression = get_end_expression(list_ends, p)
1992
2003
  if p_end_expression:
1993
2004
  end_expressions.append(p_end_expression)
@@ -2259,7 +2270,12 @@ def get_none_option(node):
2259
2270
  def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=False, process=None):
2260
2271
  opt_none = get_none_option(prev_node)
2261
2272
  if opt_none:
2262
- operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic(opt_none)])
2273
+ if isinstance(opt_none, str):
2274
+ operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic(opt_none)])
2275
+ elif issubclass(opt_none.__class__, TriccBaseModel):
2276
+ operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, opt_none])
2277
+ else:
2278
+ logger.critical(f"unexpected none option value {opt_none}")
2263
2279
  else:
2264
2280
  operation_none = TriccOperation(TriccOperator.SELECTED, [prev_node, TriccStatic("opt_none")])
2265
2281
  if isinstance(prev_node, TriccNodeSelectYesNo):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.8
3
+ Version: 1.6.14
4
4
  Summary: Python library that converts CDSS L2 in L3
5
5
  Project-URL: Homepage, https://github.com/SwissTPH/tricc
6
6
  Project-URL: Issues, https://github.com/SwissTPH/tricc/issues
@@ -1,16 +1,15 @@
1
1
  tests/build.py,sha256=Qbxvjkj_Wk2nQ-WjaMGiE1FIe3SRmJMRIgeoMoxqlfQ,6748
2
- tests/test_build.py,sha256=5t8iliPe_0XwoZjSGkHxUbZaNOWBfc6SpIQijh9DLUA,10037
3
2
  tests/test_cql.py,sha256=dAsLMqVaS6qxnq62fg5KqTFu6UG6pHO6Ab3NZ1c9T3Y,5248
4
3
  tests/to_ocl.py,sha256=4e-i65K3UM6wHgdVcrZcM9AyL1bahIsXJiZTXhhHgQk,2048
5
4
  tricc_oo/__init__.py,sha256=oWCE1ubmC_6iqaWOMgTei4eXVQgV202Ia-tXS1NnW_4,139
6
5
  tricc_oo/converters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQTMZBjWWbamYE,6151
6
+ tricc_oo/converters/codesystem_to_ocl.py,sha256=V_oZNVUVaYHgzJtDiBMSrvIJnBWmDNFBDTTHEkOfpXk,6151
8
7
  tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
9
- tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
10
- tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
11
- tricc_oo/converters/tricc_to_xls_form.py,sha256=wsWv4aA0QssY7ry9R7KsuuMzVfovj9fwE3i9AtCum0c,3842
8
+ tricc_oo/converters/datadictionnary.py,sha256=JasqlLKiZzKdidsA1xc2SJ_Af1Xr6A3sKfzDynto8Ho,3686
9
+ tricc_oo/converters/drawio_type_map.py,sha256=Zp8J9iHNSJkIVrmRSM0_d4vA1X8wFPLKb8nCMPUMXKU,9114
10
+ tricc_oo/converters/tricc_to_xls_form.py,sha256=39hwWgYNitGE-AuKtjUwNLz39tEpwc7nd9gT_gw5wjc,3842
12
11
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
13
- tricc_oo/converters/xml_to_tricc.py,sha256=PEBe8N-JIGJMVX2FO3UVxRcy5GrGsMvgcArEwUXmr6o,39572
12
+ tricc_oo/converters/xml_to_tricc.py,sha256=ea8LNEPDe32q74AJCbEjxaLt_Po47oH45K_G8fo7TzE,40388
14
13
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
15
14
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
16
15
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
@@ -26,7 +25,7 @@ tricc_oo/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
26
25
  tricc_oo/parsers/xml.py,sha256=uzkb1y18MHfqVFmZqVh0sKT4cx6u0-NcAT_lV_gHBt8,4208
27
26
  tricc_oo/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
27
  tricc_oo/serializers/planuml.py,sha256=t57587-6L3aDncpHh58lS77Zft8yxDE9DPtXx2BeUSU,132
29
- tricc_oo/serializers/xls_form.py,sha256=L0WF774zFt6PbdqncJGeyZzM9NiLq9NY5vO98yGTLhM,22133
28
+ tricc_oo/serializers/xls_form.py,sha256=ydaD5l_CsMEhJlSO0XvXPdxrwsDw4HiRjTOPHogTQdw,23071
30
29
  tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
30
  tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
31
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
@@ -39,14 +38,14 @@ tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5si
39
38
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
40
39
  tricc_oo/strategies/output/xls_form.py,sha256=_pNTND7n-55EjRphJ1hSVtRYa-UkXlmwpam2OKQ8o_w,30860
41
40
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
42
- tricc_oo/strategies/output/xlsform_cht.py,sha256=eKAc6LLDnvdZ5m8a2Vk6eAhvPbUvOfykgYr0ou3an9k,27164
41
+ tricc_oo/strategies/output/xlsform_cht.py,sha256=Zy8aggR1sTBP0b33RGbfUpk8pRppI1LGQEif4E1l49A,28523
43
42
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
44
43
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- tricc_oo/visitors/tricc.py,sha256=8fULbAxKSeEtPyD5TYTaIG8-20Bjg2iZmzxcJM70n18,110295
44
+ tricc_oo/visitors/tricc.py,sha256=QkVZpmDvSrMbMQeCxVn15zSk3iH-3-e-udMys96JkXE,110923
46
45
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
47
46
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
48
- tricc_oo-1.6.8.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.8.dist-info/METADATA,sha256=g9u0_NdvNT8FFgNB3LQzsUADmGqL1PBB1DPdnCBKVuI,8599
50
- tricc_oo-1.6.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.8.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.8.dist-info/RECORD,,
47
+ tricc_oo-1.6.14.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
48
+ tricc_oo-1.6.14.dist-info/METADATA,sha256=_WkaLTsN1Yj8koCLVrOmdY9egk06du90gsMqRCcQ0Js,8600
49
+ tricc_oo-1.6.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
50
+ tricc_oo-1.6.14.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
51
+ tricc_oo-1.6.14.dist-info/RECORD,,
tests/test_build.py DELETED
@@ -1,260 +0,0 @@
1
- import unittest
2
- import subprocess
3
- import sys
4
- import os
5
- import tempfile
6
- import shutil
7
- from pathlib import Path
8
- import pandas as pd
9
- from pyxform import create_survey_from_xls
10
-
11
-
12
- class TestBuildScript(unittest.TestCase):
13
- """Test cases for the build.py script with different argument combinations."""
14
-
15
- def setUp(self):
16
- """Set up test fixtures."""
17
- self.test_data_dir = Path(__file__).parent / "data"
18
- self.test_output_dir = Path(__file__).parent / "output"
19
- self.demo_file = self.test_data_dir / "demo.drawio"
20
-
21
- # Ensure test data exists
22
- self.assertTrue(self.demo_file.exists(), f"Test data file {self.demo_file} does not exist")
23
-
24
- def run_build_script(self, args):
25
- """Helper method to run build.py with given arguments."""
26
- cmd = [sys.executable, str(Path(__file__).parent / "build.py")] + args
27
- result = subprocess.run(cmd, capture_output=True, text=True, cwd=Path(__file__).parent.parent)
28
- return result
29
-
30
- def test_basic_build_with_demo_file(self):
31
- """Test basic build with demo.drawio file."""
32
- with tempfile.TemporaryDirectory() as temp_dir:
33
- args = [
34
- "-i", str(self.demo_file),
35
- "-o", temp_dir,
36
- "-l", "i"
37
- ]
38
- result = self.run_build_script(args)
39
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
40
-
41
- def test_build_with_directory_input(self):
42
- """Test build with directory containing drawio files."""
43
- with tempfile.TemporaryDirectory() as temp_dir:
44
- args = [
45
- "-i", str(self.test_data_dir),
46
- "-o", temp_dir,
47
- "-l", "i"
48
- ]
49
- result = self.run_build_script(args)
50
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
51
-
52
- def test_build_with_xlsform_strategy(self):
53
- """Test build with XLSFormStrategy (default)."""
54
- with tempfile.TemporaryDirectory() as temp_dir:
55
- args = [
56
- "-i", str(self.demo_file),
57
- "-o", temp_dir,
58
- "-O", "XLSFormStrategy",
59
- "-l", "i"
60
- ]
61
- result = self.run_build_script(args)
62
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
63
-
64
- def test_build_with_html_strategy(self):
65
- """Test build with HTMLStrategy."""
66
- with tempfile.TemporaryDirectory() as temp_dir:
67
- args = [
68
- "-i", str(self.demo_file),
69
- "-o", temp_dir,
70
- "-O", "HTMLStrategy",
71
- "-l", "i"
72
- ]
73
- result = self.run_build_script(args)
74
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
75
-
76
- def test_build_with_fhir_strategy(self):
77
- """Test build with FHIRStrategy."""
78
- with tempfile.TemporaryDirectory() as temp_dir:
79
- args = [
80
- "-i", str(self.demo_file),
81
- "-o", temp_dir,
82
- "-O", "FHIRStrategy",
83
- "-l", "i"
84
- ]
85
- result = self.run_build_script(args)
86
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
87
-
88
- def test_build_with_dhis2_strategy(self):
89
- """Test build with DHIS2Strategy."""
90
- with tempfile.TemporaryDirectory() as temp_dir:
91
- args = [
92
- "-i", str(self.demo_file),
93
- "-o", temp_dir,
94
- "-O", "DHIS2Strategy",
95
- "-l", "i"
96
- ]
97
- result = self.run_build_script(args)
98
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
99
-
100
- def test_build_with_openmrs_strategy(self):
101
- """Test build with OpenMRSStrategy."""
102
- with tempfile.TemporaryDirectory() as temp_dir:
103
- args = [
104
- "-i", str(self.demo_file),
105
- "-o", temp_dir,
106
- "-O", "OpenMRSStrategy",
107
- "-l", "i"
108
- ]
109
- result = self.run_build_script(args)
110
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
111
-
112
- def test_build_with_cht_strategy(self):
113
- """Test build with XLSFormCHTStrategy."""
114
- with tempfile.TemporaryDirectory() as temp_dir:
115
- args = [
116
- "-i", str(self.demo_file),
117
- "-o", temp_dir,
118
- "-O", "XLSFormCHTStrategy",
119
- "-l", "i"
120
- ]
121
- result = self.run_build_script(args)
122
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
123
-
124
- def test_build_with_cht_hf_strategy(self):
125
- """Test build with XLSFormCHTHFStrategy."""
126
- with tempfile.TemporaryDirectory() as temp_dir:
127
- args = [
128
- "-i", str(self.demo_file),
129
- "-o", temp_dir,
130
- "-O", "XLSFormCHTHFStrategy",
131
- "-l", "i"
132
- ]
133
- result = self.run_build_script(args)
134
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
135
-
136
- def test_build_with_cdss_strategy(self):
137
- """Test build with XLSFormCDSSStrategy."""
138
- with tempfile.TemporaryDirectory() as temp_dir:
139
- args = [
140
- "-i", str(self.demo_file),
141
- "-o", temp_dir,
142
- "-O", "XLSFormCDSSStrategy",
143
- "-l", "i"
144
- ]
145
- result = self.run_build_script(args)
146
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
147
-
148
- # def test_build_with_spice_strategy(self):
149
- # """Test build with SpiceStrategy."""
150
- # with tempfile.TemporaryDirectory() as temp_dir:
151
- # args = [
152
- # "-i", str(self.demo_file),
153
- # "-o", temp_dir,
154
- # "-O", "SpiceStrategy",
155
- # "-l", "i"
156
- # ]
157
- # result = self.run_build_script(args)
158
- # self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
159
-
160
- def test_build_with_debug_level(self):
161
- """Test build with debug logging level."""
162
- with tempfile.TemporaryDirectory() as temp_dir:
163
- args = [
164
- "-i", str(self.demo_file),
165
- "-o", temp_dir,
166
- "-l", "d"
167
- ]
168
- result = self.run_build_script(args)
169
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
170
-
171
- def test_build_with_trad_option(self):
172
- """Test build with translation option."""
173
- with tempfile.TemporaryDirectory() as temp_dir:
174
- args = [
175
- "-i", str(self.demo_file),
176
- "-o", temp_dir,
177
- "-t",
178
- "-l", "i"
179
- ]
180
- result = self.run_build_script(args)
181
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
182
-
183
- def test_build_with_form_id(self):
184
- """Test build with custom form ID."""
185
- with tempfile.TemporaryDirectory() as temp_dir:
186
- args = [
187
- "-i", str(self.demo_file),
188
- "-o", temp_dir,
189
- "-d", "test_form_123",
190
- "-l", "i"
191
- ]
192
- result = self.run_build_script(args)
193
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
194
-
195
- def test_build_missing_input(self):
196
- """Test build with missing input file."""
197
- args = ["-o", "/tmp/test_output"]
198
- result = self.run_build_script(args)
199
- self.assertNotEqual(result.returncode, 0, "Build should fail with missing input")
200
-
201
- def test_build_invalid_input_file(self):
202
- """Test build with invalid input file."""
203
- with tempfile.TemporaryDirectory() as temp_dir:
204
- args = [
205
- "-i", "/nonexistent/file.drawio",
206
- "-o", temp_dir,
207
- "-l", "i"
208
- ]
209
- result = self.run_build_script(args)
210
- self.assertNotEqual(result.returncode, 0, "Build should fail with invalid input file")
211
-
212
- def validate_xls_form(self, xls_path):
213
- """Helper method to validate XLS form using ODK libraries."""
214
- try:
215
- # Convert XLS to XML using pyxform
216
- survey = create_survey_from_xls(xls_path)
217
- xml_output = survey.to_xml()
218
-
219
- # Basic validation - check if XML was generated successfully
220
- # In a real scenario, you might want to use odk_validate command line tool
221
- if xml_output and len(xml_output.strip()) > 0:
222
- return True, "Validation successful"
223
- else:
224
- return False, "Empty XML output"
225
- except Exception as e:
226
- return False, str(e)
227
-
228
- def test_xlsform_strategy_validation(self):
229
- """Test XLSFormStrategy output validation with ODK libraries."""
230
- with tempfile.TemporaryDirectory() as temp_dir:
231
- xls_output = Path(temp_dir) / "demo_tricc.xlsx"
232
- args = [
233
- "-i", str(self.demo_file),
234
- "-o", temp_dir,
235
- "-O", "XLSFormStrategy",
236
- "-l", "i"
237
- ]
238
- result = self.run_build_script(args)
239
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
240
-
241
- # Check if XLS file was created
242
- self.assertTrue(xls_output.exists(), f"XLS file {xls_output} was not created")
243
-
244
- def test_xlsform_cdss_strategy_validation(self):
245
- """Test XLSFormCDSSStrategy output validation with ODK libraries."""
246
- with tempfile.TemporaryDirectory() as temp_dir:
247
- xls_output = Path(temp_dir) / "demo_tricc.xlsx"
248
- args = [
249
- "-i", str(self.demo_file),
250
- "-o", temp_dir,
251
- "-O", "XLSFormCDSSStrategy",
252
- "-l", "i"
253
- ]
254
- result = self.run_build_script(args)
255
- self.assertEqual(result.returncode, 0, f"Build failed: {result.stderr}")
256
-
257
-
258
-
259
- if __name__ == "__main__":
260
- unittest.main()