tricc-oo 1.6.6__tar.gz → 1.6.8__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 (57) hide show
  1. {tricc_oo-1.6.6/tricc_oo.egg-info → tricc_oo-1.6.8}/PKG-INFO +1 -1
  2. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/pyproject.toml +1 -1
  3. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/tricc_to_xls_form.py +2 -3
  4. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/xml_to_tricc.py +2 -2
  5. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/serializers/xls_form.py +10 -10
  6. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/base_output_strategy.py +7 -0
  7. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/dhis2_form.py +8 -8
  8. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/openmrs_form.py +2 -2
  9. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/xls_form.py +34 -3
  10. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/xlsform_cht.py +104 -0
  11. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/visitors/tricc.py +19 -9
  12. {tricc_oo-1.6.6 → tricc_oo-1.6.8/tricc_oo.egg-info}/PKG-INFO +1 -1
  13. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/LICENSE +0 -0
  14. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/README.md +0 -0
  15. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/setup.cfg +0 -0
  16. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tests/build.py +0 -0
  17. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tests/test_build.py +0 -0
  18. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tests/test_cql.py +0 -0
  19. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tests/to_ocl.py +0 -0
  20. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/__init__.py +0 -0
  21. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/__init__.py +0 -0
  22. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/codesystem_to_ocl.py +0 -0
  23. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/cql/cqlLexer.py +0 -0
  24. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/cql/cqlListener.py +0 -0
  25. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/cql/cqlParser.py +0 -0
  26. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/cql/cqlVisitor.py +0 -0
  27. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/cql_to_operation.py +0 -0
  28. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/datadictionnary.py +0 -0
  29. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/drawio_type_map.py +0 -0
  30. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/converters/utils.py +0 -0
  31. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/__init__.py +0 -0
  32. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/base.py +0 -0
  33. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/calculate.py +0 -0
  34. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/lang.py +0 -0
  35. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/ocl.py +0 -0
  36. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/ordered_set.py +0 -0
  37. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/models/tricc.py +0 -0
  38. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/parsers/__init__.py +0 -0
  39. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/parsers/xml.py +0 -0
  40. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/serializers/__init__.py +0 -0
  41. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/serializers/planuml.py +0 -0
  42. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/__init__.py +0 -0
  43. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/input/__init__.py +0 -0
  44. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/input/base_input_strategy.py +0 -0
  45. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/input/drawio.py +0 -0
  46. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/fhir_form.py +0 -0
  47. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/html_form.py +0 -0
  48. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/spice.py +0 -0
  49. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/xlsform_cdss.py +0 -0
  50. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/strategies/output/xlsform_cht_hf.py +0 -0
  51. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/visitors/__init__.py +0 -0
  52. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/visitors/utils.py +0 -0
  53. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo/visitors/xform_pd.py +0 -0
  54. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo.egg-info/SOURCES.txt +0 -0
  55. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo.egg-info/dependency_links.txt +0 -0
  56. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo.egg-info/requires.txt +0 -0
  57. {tricc_oo-1.6.6 → tricc_oo-1.6.8}/tricc_oo.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.6
3
+ Version: 1.6.8
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tricc-oo"
7
- version = "1.6.6"
7
+ version = "1.6.8"
8
8
  description = "Python library that converts CDSS L2 in L3"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,5 +1,5 @@
1
1
  import logging
2
- from tricc_oo.converters.utils import clean_name, clean_str
2
+ from tricc_oo.converters.utils import clean_name
3
3
  from tricc_oo.models.tricc import TriccNodeSelectOption, TRICC_TRUE_VALUE, TRICC_FALSE_VALUE, TriccNodeActivity
4
4
  from tricc_oo.models.calculate import TriccNodeInput
5
5
  from tricc_oo.models.base import TriccNodeBaseModel, TriccStatic, TriccReference
@@ -66,14 +66,13 @@ def get_export_name(node, replace_dots=True):
66
66
  return node
67
67
  else:
68
68
  node.gen_name()
69
- if isinstance(node, TriccNodeActivity) and getattr(node, 'instance', 1)>1:
69
+ if isinstance(node, TriccNodeActivity) and getattr(node, 'instance', 1) > 1:
70
70
  node.export_name = clean_name(
71
71
  node.name + INSTANCE_SEPARATOR + str(node.instance),
72
72
  replace_dots=replace_dots,
73
73
  )
74
74
  elif isinstance(node, TriccNodeSelectOption):
75
75
  node.export_name = node.name
76
-
77
76
  elif node.last is False:
78
77
  node.export_name = clean_name(
79
78
  node.name + VERSION_SEPARATOR + str(node.version),
@@ -7,7 +7,7 @@ import re
7
7
 
8
8
  from tricc_oo.converters.utils import remove_html, clean_str
9
9
  from tricc_oo.converters.cql_to_operation import transform_cql_to_operation
10
- from tricc_oo.converters.utils import generate_id, get_rand_name
10
+ from tricc_oo.converters.utils import generate_id
11
11
  from tricc_oo.models.base import (
12
12
  TriccOperator, TriccOperation,
13
13
  TriccStatic, TriccReference, TriccNodeType, TriccEdge, OPERATION_LIST
@@ -107,7 +107,7 @@ def create_activity(diagram, media_path, project):
107
107
  if root is not None:
108
108
  activity = TriccNodeActivity(
109
109
  root=root,
110
- name=name, # start node 'name' is saved in label
110
+ name=name, # start node 'name' is saved in label
111
111
  id=id,
112
112
  external_id=external_id,
113
113
  label=label,
@@ -16,7 +16,7 @@ from tricc_oo.models.calculate import (
16
16
  TriccNodeCalculate
17
17
  )
18
18
  from tricc_oo.models.tricc import (
19
- TriccNodeActivity, TriccNodeBaseModel, TriccNodeSelectMultiple, TriccNodeSelectOption,
19
+ TriccNodeBaseModel, TriccNodeSelectMultiple, TriccNodeSelectOption,
20
20
  TriccNodeSelectOne,
21
21
  TriccNodeSelect,
22
22
  TriccNodeMoreInfo,
@@ -39,7 +39,9 @@ logger = logging.getLogger("default")
39
39
  langs = SingletonLangClass()
40
40
  TRICC_CALC_EXPRESSION = "${{{0}}}>0"
41
41
 
42
- def get_export_group_name(in_node):return f"gcalc_{get_export_name(in_node)}"
42
+
43
+ def get_export_group_name(in_node): return f"gcalc_{get_export_name(in_node)}"
44
+
43
45
 
44
46
  def start_group(
45
47
  strategy,
@@ -60,13 +62,12 @@ def start_group(
60
62
 
61
63
  else:
62
64
  groups[name] = 0
63
- is_activity = isinstance(cur_group, TriccNodeActivity)
64
65
  relevance = relevance and cur_group.relevance is not None and cur_group.relevance != ""
65
66
  past_instances = len(getattr(cur_group.base_instance, "instances", []))
66
- group_calc_required = relevance is not None and (len(str(relevance)) > 100 or past_instances>1)
67
+ group_calc_required = relevance is not None and (len(str(relevance)) > 100 or past_instances > 1)
67
68
  calc = None
68
69
  if group_calc_required and getattr(cur_group.relevance, 'operator', None) != TriccOperator.ISTRUE:
69
-
70
+
70
71
  calc = TriccNodeCalculate(
71
72
  id=generate_id(get_export_group_name(name)),
72
73
  group=cur_group,
@@ -77,23 +78,22 @@ def start_group(
77
78
  if calc not in cur_group.calculates:
78
79
  cur_group.calculates.append(calc)
79
80
  processed_nodes.add(calc)
80
-
81
+
81
82
  cur_group.relevance = TriccOperation(
82
83
  TriccOperator.ISTRUE,
83
84
  [calc]
84
85
  )
85
-
86
+
86
87
  relevance_expression = cur_group.relevance
87
88
  relevance_expression = get_applicability_expression(cur_group, processed_nodes, process, relevance_expression)
88
89
  relevance_expression = get_prev_instance_skip_expression(cur_group, processed_nodes, process, relevance_expression)
89
90
  relevance_expression = get_process_skip_expression(cur_group, processed_nodes, process, relevance_expression)
90
91
 
91
-
92
92
  if not relevance:
93
93
  relevance_expression_str = ""
94
94
  elif isinstance(relevance_expression, (TriccOperation, TriccStatic)):
95
95
  relevance_expression_str = strategy.get_tricc_operation_expression(relevance_expression)
96
-
96
+
97
97
  # group
98
98
  values = []
99
99
  for column in SURVEY_MAP:
@@ -114,7 +114,7 @@ def start_group(
114
114
  df_survey.loc[len(df_survey)] = values
115
115
 
116
116
  # calc
117
- if calc and len(df_calculate[df_calculate["name"] == get_export_group_name(name)]) == 0:
117
+ if calc and len(df_calculate[df_calculate["name"] == get_export_group_name(name)]) == 0:
118
118
  calc_values = []
119
119
  for column in SURVEY_MAP:
120
120
  if column == "type":
@@ -43,6 +43,9 @@ class BaseOutPutStrategy:
43
43
 
44
44
  self.export(self.project.start_pages, version=version)
45
45
 
46
+ logger.info("validate the output")
47
+ self.validate()
48
+
46
49
  # walking function
47
50
  def process_base(self, start_pages, **kwargs):
48
51
  # for each node, check if condition is required issubclass(TriccNodeDisplayModel)
@@ -106,6 +109,10 @@ class BaseOutPutStrategy:
106
109
  def export(self, **kwargs):
107
110
  pass
108
111
 
112
+ @abc.abstractmethod
113
+ def validate(self):
114
+ pass
115
+
109
116
  def tricc_operation_equal(self, ref_expressions):
110
117
  # r[0] = r[1]
111
118
  raise NotImplementedError("This type of opreration is not supported in this strategy")
@@ -145,15 +145,16 @@ class DHIS2Strategy(BaseOutPutStrategy):
145
145
  raise NotImplementedError(
146
146
  f"This type of operation '{operation.operator}' is not supported in this strategy"
147
147
  )
148
+
148
149
  def get_display(self, node):
149
150
  if hasattr(node, 'label') and node.label:
150
- ret = node.label
151
+ ret = node.label
151
152
  elif hasattr(node, 'name') and node.name:
152
153
  ret = node.name
153
154
  else:
154
155
  ret = str(node.id)
155
- return ret.replace('\u00a0', ' ').strip()
156
-
156
+ return ret.replace('\u00a0', ' ').strip()
157
+
157
158
  def execute(self):
158
159
  version = datetime.datetime.now().strftime("%Y%m%d%H%M")
159
160
  logger.info(f"build version: {version}")
@@ -273,13 +274,12 @@ class DHIS2Strategy(BaseOutPutStrategy):
273
274
  "id": de_id,
274
275
  "name": self.get_export_name(node),
275
276
  "shortName": node.name[:50],
276
- "displayFormName":self.get_display(node),
277
+ "displayFormName": self.get_display(node),
277
278
  "formName": self.get_display(node),
278
279
  "valueType": value_type,
279
280
  "domainType": "TRACKER",
280
281
  "aggregationType": "NONE"
281
282
  }
282
-
283
283
  if issubclass(node.__class__, TriccNodeSelect) and not isinstance(node, TriccNodeSelectYesNo):
284
284
  data_element["optionSetValue"] = True
285
285
 
@@ -736,15 +736,15 @@ class DHIS2Strategy(BaseOutPutStrategy):
736
736
  return f"#{{{node_id}}}"
737
737
  elif issubclass(r.__class__, TriccNodeCalculateBase):
738
738
  # Use variable name from concept_map
739
- node_id = self.get_export_name(r)
739
+ node_id = self.get_export_name(r)
740
740
  return f"#{{{node_id}}}"
741
741
  elif issubclass(r.__class__, TriccNodeInputModel):
742
742
  # Use variable name from concept_map
743
- node_id = self.get_export_name(r)
743
+ node_id = self.get_export_name(r)
744
744
  return f"#{{{node_id}}}"
745
745
  elif issubclass(r.__class__, TriccNodeBaseModel):
746
746
  # Use variable name from concept_map
747
- node_id = self.get_export_name(r)
747
+ node_id = self.get_export_name(r)
748
748
  return f"#{{{node_id}}}"
749
749
  else:
750
750
  raise NotImplementedError(f"This type of node {r.__class__.__name__} is not supported within an operation")
@@ -410,7 +410,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
410
410
  elif issubclass(r.__class__, TriccNodeInputModel):
411
411
  return self.get_export_name(r)
412
412
  elif issubclass(r.__class__, TriccNodeSelect):
413
- return "(" + self.get_export_name(r) + " ?? [])"
413
+ return "(" + self.get_export_name(r) + " ?? [])"
414
414
  elif issubclass(r.__class__, TriccNodeBaseModel):
415
415
  return self.get_export_name(r)
416
416
  else:
@@ -458,7 +458,7 @@ class OpenMRSStrategy(BaseOutPutStrategy):
458
458
  return f"!({ref_expressions[0]})"
459
459
 
460
460
  def tricc_operation_plus(self, ref_expressions):
461
- return "(" + " + ".join(ref_expressions) +")"
461
+ return "(" + " + ".join(ref_expressions) + ")"
462
462
 
463
463
  def tricc_operation_minus(self, ref_expressions):
464
464
  if len(ref_expressions) > 1:
@@ -8,6 +8,7 @@ import logging
8
8
  import os
9
9
  import re
10
10
  import pandas as pd
11
+ from pyxform import create_survey_from_xls
11
12
 
12
13
  from tricc_oo.converters.utils import clean_name
13
14
  from tricc_oo.models.base import (
@@ -409,7 +410,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
409
410
  return f"count-selected({self.clean_coalesce(ref_expressions[0])})"
410
411
 
411
412
  def tricc_operation_multiplied(self, ref_expressions):
412
- return "*".join(map(str,ref_expressions))
413
+ return "*".join(map(str, ref_expressions))
413
414
 
414
415
  def tricc_operation_divided(self, ref_expressions):
415
416
  return f"{ref_expressions[0]} div {ref_expressions[1]}"
@@ -430,7 +431,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
430
431
  return f"-{ref_expressions[0]}"
431
432
 
432
433
  def tricc_operation_plus(self, ref_expressions):
433
- return " + ".join(map(str,ref_expressions))
434
+ return " + ".join(map(str, ref_expressions))
434
435
 
435
436
  def tricc_operation_not(self, ref_expressions):
436
437
  return f"not({ref_expressions[0]})"
@@ -474,7 +475,7 @@ class XLSFormStrategy(BaseOutPutStrategy):
474
475
  return "0"
475
476
  # return f"jr:choice-name({','.join(ref_expressions[1:])})"
476
477
  else:
477
- return f"{ref_expressions[0]}({','.join(map(str,ref_expressions[1:]))})"
478
+ return f"{ref_expressions[0]}({','.join(map(str, ref_expressions[1:]))})"
478
479
 
479
480
  def tricc_operation_istrue(self, ref_expressions):
480
481
  if str(BOOLEAN_MAP[str(TRICC_TRUE_VALUE)]).isnumeric():
@@ -747,3 +748,33 @@ class XLSFormStrategy(BaseOutPutStrategy):
747
748
 
748
749
  def tricc_operation_concatenate(self, ref_expressions):
749
750
  return f"concat({','.join(map(str, ref_expressions))})"
751
+
752
+ def validate(self):
753
+ """Validate the generated XLS form using pyxform."""
754
+ try:
755
+ # Determine the XLS file path
756
+ if self.project.start_pages["main"].root.form_id is not None:
757
+ form_id = str(self.project.start_pages["main"].root.form_id)
758
+ xls_path = os.path.join(self.output_path, form_id + ".xlsx")
759
+
760
+ if not os.path.exists(xls_path):
761
+ logger.error(f"XLS file not found: {xls_path}")
762
+ return False
763
+
764
+ # Validate using pyxform
765
+ survey = create_survey_from_xls(xls_path)
766
+ xml_output = survey.to_xml()
767
+
768
+ # Check if XML was generated successfully
769
+ if xml_output and len(xml_output.strip()) > 0:
770
+ logger.info("XLSForm validation successful")
771
+ return True
772
+ else:
773
+ logger.error("XLSForm validation failed: Empty XML output")
774
+ return False
775
+ else:
776
+ logger.error("Form ID not found for validation")
777
+ return False
778
+ except Exception as e:
779
+ logger.error(f"XLSForm validation failed: {str(e)}")
780
+ return False
@@ -2,6 +2,7 @@ import datetime
2
2
  import logging
3
3
  import os
4
4
  import shutil
5
+ import subprocess
5
6
  import pandas as pd
6
7
 
7
8
  from tricc_oo.models.lang import SingletonLangClass
@@ -616,6 +617,9 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
616
617
  newpath = os.path.join(self.output_path, newfilename)
617
618
  media_path = os.path.join(self.output_path, form_id + "-media")
618
619
 
620
+ # Track all generated XLS files for validation
621
+ generated_files = [newpath]
622
+
619
623
  settings = {
620
624
  "form_title": title,
621
625
  "form_id": form_id,
@@ -660,6 +664,7 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
660
664
  new_form_id = f"{form_id}_{clean_name(e.name)}"
661
665
  newfilename = f"{new_form_id}.xlsx"
662
666
  newpath = os.path.join(self.output_path, newfilename)
667
+ generated_files.append(newpath) # Track additional XLS files
663
668
  settings = {
664
669
  "form_title": title,
665
670
  "form_id": f"{new_form_id}",
@@ -708,6 +713,105 @@ class XLSFormCHTStrategy(XLSFormCDSSStrategy):
708
713
  shutil.move(os.path.join(media_path_tmp, file_name), media_path)
709
714
  shutil.rmtree(media_path_tmp)
710
715
 
716
+ return generated_files
717
+
718
+ def execute(self):
719
+ """Override execute to handle multiple output files from CHT strategy."""
720
+ version = datetime.datetime.now().strftime("%Y%m%d%H%M")
721
+ logger.info(f"build version: {version}")
722
+ if "main" in self.project.start_pages:
723
+ self.process_base(self.project.start_pages, pages=self.project.pages, version=version)
724
+ else:
725
+ logger.critical("Main process required")
726
+
727
+ logger.info("generate the relevance based on edges")
728
+
729
+ # create relevance Expression
730
+
731
+ # create calculate Expression
732
+ self.process_calculate(self.project.start_pages, pages=self.project.pages)
733
+ logger.info("generate the export format")
734
+ # create calculate Expression
735
+ self.process_export(self.project.start_pages, pages=self.project.pages)
736
+
737
+ logger.info("print the export")
738
+
739
+ # Export returns list of generated files for CHT strategy
740
+ generated_files = self.export(self.project.start_pages, version=version)
741
+
742
+ logger.info("validate the output")
743
+ self.validate(generated_files)
744
+
745
+ def validate(self, generated_files=None):
746
+ """Validate the generated XLS form(s) using xls2xform-medic."""
747
+ if generated_files is None:
748
+ # Fallback for single file validation
749
+ if self.project.start_pages["main"].root.form_id is not None:
750
+ form_id = str(self.project.start_pages["main"].root.form_id)
751
+ generated_files = [os.path.join(self.output_path, form_id + ".xlsx")]
752
+ else:
753
+ logger.error("Form ID not found for validation")
754
+ return False
755
+
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")
760
+ return False
761
+
762
+ all_valid = True
763
+ for xls_file in generated_files:
764
+ if not os.path.exists(xls_file):
765
+ logger.error(f"XLS file not found: {xls_file}")
766
+ all_valid = False
767
+ continue
768
+
769
+ 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
+ )
777
+
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
783
+
784
+ except Exception as e:
785
+ logger.error(f"CHT XLSForm validation error for {os.path.basename(xls_file)}: {str(e)}")
786
+ all_valid = False
787
+
788
+ return all_valid
789
+
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
796
+
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
801
+
802
+ # Try to download from the provided URL
803
+ try:
804
+ 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
811
+ except Exception as e:
812
+ logger.error(f"Failed to download xls2xform-medic: {str(e)}")
813
+ return None
814
+
711
815
  def tricc_operation_zscore(self, ref_expressions):
712
816
  y, ll, m, s = self.get_zscore_params(ref_expressions)
713
817
  # return ((Math.pow((y / m), l) - 1) / (s * l));
@@ -121,7 +121,10 @@ def get_last_version(name, processed_nodes, _list=None):
121
121
  # node is the node to calculate
122
122
  # processed_nodes are the list of processed nodes
123
123
  def get_node_expressions(node, processed_nodes, process=None):
124
- get_overall_exp = issubclass(node.__class__, (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis)) and not isinstance(node, (TriccNodeDisplayBridge))
124
+ get_overall_exp = issubclass(
125
+ node.__class__,
126
+ (TriccNodeDisplayCalculateBase, TriccNodeProposedDiagnosis, TriccNodeDiagnosis)
127
+ ) and not isinstance(node, (TriccNodeDisplayBridge))
125
128
  expression = None
126
129
  # in case of recursive call processed_nodes will be None
127
130
  if processed_nodes is None or is_ready_to_process(node, processed_nodes=processed_nodes):
@@ -173,8 +176,8 @@ def get_version_inheritance(node, last_version, processed_nodes):
173
176
  node.relevance = expression
174
177
  else:
175
178
  node.last = False
176
-
177
- # Create a calculate node that coalesces the previous saved value with the current node value
179
+
180
+ # Create a calculate node that coalesces the previous saved value with the current node value
178
181
  calc_id = generate_id(f"save_{node.save}")
179
182
  calc = TriccNodeCalculate(
180
183
  id=calc_id,
@@ -184,10 +187,10 @@ def get_version_inheritance(node, last_version, processed_nodes):
184
187
  TriccOperator.COALESCE,
185
188
  [TriccReference(node.save), last_version],
186
189
  ),
187
- reference=[TriccReference(n.name)],
190
+ reference=[TriccReference(node.name)],
188
191
  activity=node.activity,
189
192
  group=node.group,
190
- label=f"Save calculation for {n.label}",
193
+ label=f"Save calculation for {node.label}",
191
194
  last=True,
192
195
  )
193
196
  node.activity.nodes[calc.id] = calc
@@ -1765,7 +1768,10 @@ def reorder_node_list(node_list, group, processed_nodes):
1765
1768
  elif activity and isinstance(activity.root, TriccNodeActivityStart):
1766
1769
  priority += NON_START_ACTIVITY_PRIORITY
1767
1770
  # Check for display calculate and end nodes with prev_nodes
1768
- elif (issubclass(node.__class__, TriccNodeDisplayCalculateBase) or isinstance(node, TriccNodeEnd)) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
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:
1769
1775
  priority += FLOW_CALCULATE_NODE_PRIORITY
1770
1776
  # Check for active activities (lower priority)
1771
1777
  elif activity and activity in active_activities:
@@ -2100,7 +2106,7 @@ def create_determine_diagnosis_activity(diags):
2100
2106
  )
2101
2107
  options = []
2102
2108
  for proposed in diags:
2103
- option = TriccNodeSelectOption(
2109
+ option = TriccNodeSelectOption(
2104
2110
  id=generate_id(proposed.name),
2105
2111
  name=proposed.name,
2106
2112
  label=proposed.label,
@@ -2108,6 +2114,7 @@ def create_determine_diagnosis_activity(diags):
2108
2114
  relevance=proposed.activity.applicability,
2109
2115
  select=f,
2110
2116
  )
2117
+ options.append(option)
2111
2118
  d = get_accept_diagnostic_node(proposed.name, proposed.label, proposed.severity, proposed.priority, activity)
2112
2119
  c = get_diagnostic_node(proposed.name, proposed.label, proposed.severity, proposed.priority, activity, option)
2113
2120
  diags_conf.append(d)
@@ -2134,7 +2141,6 @@ def create_determine_diagnosis_activity(diags):
2134
2141
  activity.nodes[wait2.id] = wait2
2135
2142
  # fallback
2136
2143
 
2137
-
2138
2144
  f.options = dict(zip(range(0, len(options)), options))
2139
2145
  activity.nodes[f.id] = f
2140
2146
  set_prev_next_node(f, end, edge_only=False)
@@ -2307,7 +2313,11 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
2307
2313
  TriccOperator.CAST_NUMBER,
2308
2314
  [
2309
2315
  get_node_expression(
2310
- prev_node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, is_prev=True, process=process
2316
+ prev_node,
2317
+ processed_nodes=processed_nodes,
2318
+ get_overall_exp=get_overall_exp,
2319
+ is_prev=True,
2320
+ process=process
2311
2321
  )
2312
2322
  ],
2313
2323
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.6
3
+ Version: 1.6.8
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes