tricc-oo 1.6.7__py3-none-any.whl → 1.6.9__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.
@@ -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,
@@ -625,6 +625,8 @@ def enrich_node(diagram, media_path, edge, node, activity, help_before=False):
625
625
  name=f"{node.name}.more_info",
626
626
  label=message,
627
627
  parent=node,
628
+ group=node.group,
629
+ activity=node.activity,
628
630
  required=None,
629
631
  )
630
632
  # node.help = message
@@ -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")
@@ -16,7 +16,7 @@ import datetime
16
16
  from tricc_oo.strategies.output.base_output_strategy import BaseOutPutStrategy
17
17
  from tricc_oo.models.base import (
18
18
  not_clean, TriccOperation,
19
- TriccStatic, TriccReference
19
+ TriccStatic, TriccReference, TriccOperator
20
20
  )
21
21
  from tricc_oo.models.tricc import (
22
22
  TriccNodeSelectOption,
@@ -27,6 +27,8 @@ from tricc_oo.models.tricc import (
27
27
  TriccNodeActivity,
28
28
  TriccNodeSelect,
29
29
  TriccNodeSelectYesNo,
30
+ TriccNodeNote,
31
+ TriccNodeMoreInfo,
30
32
  )
31
33
  from tricc_oo.models.calculate import TriccNodeDisplayCalculateBase
32
34
  from tricc_oo.models.ordered_set import OrderedSet
@@ -44,7 +46,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
44
46
 
45
47
  def __init__(self, project, output_path):
46
48
  super().__init__(project, output_path)
47
- form_id = getattr(self.project.start_pages["main"], 'form_id', 'dhis2_program')
49
+ form_id = getattr(self.project.start_pages["main"].root, 'form_id', 'dhis2_program')
48
50
  self.program_metadata = {
49
51
  "id": self.generate_id(form_id),
50
52
  "name": form_id,
@@ -145,15 +147,16 @@ class DHIS2Strategy(BaseOutPutStrategy):
145
147
  raise NotImplementedError(
146
148
  f"This type of operation '{operation.operator}' is not supported in this strategy"
147
149
  )
150
+
148
151
  def get_display(self, node):
149
152
  if hasattr(node, 'label') and node.label:
150
- ret = node.label
153
+ ret = node.label
151
154
  elif hasattr(node, 'name') and node.name:
152
155
  ret = node.name
153
156
  else:
154
157
  ret = str(node.id)
155
- return ret.replace('\u00a0', ' ').strip()
156
-
158
+ return ret.replace('\u00a0', ' ').strip()
159
+
157
160
  def execute(self):
158
161
  version = datetime.datetime.now().strftime("%Y%m%d%H%M")
159
162
  logger.info(f"build version: {version}")
@@ -203,6 +206,10 @@ class DHIS2Strategy(BaseOutPutStrategy):
203
206
  return False
204
207
 
205
208
  if node not in processed_nodes:
209
+ # Skip relevance generation for TriccNodeMoreInfo as they don't create DataElements
210
+ if isinstance(node, TriccNodeMoreInfo):
211
+ return True
212
+
206
213
  relevance = None
207
214
  if hasattr(node, 'relevance') and node.relevance:
208
215
  relevance = node.relevance
@@ -224,8 +231,10 @@ class DHIS2Strategy(BaseOutPutStrategy):
224
231
  "activity_ref": node, # Temporary reference to be replaced with section ID
225
232
  "programRule": {"id": rule_id},
226
233
  }
227
- else:
228
- # For regular nodes, use HIDEFIELD action
234
+ self.program_rule_actions.append(program_rule_action)
235
+ elif not issubclass(node.__class__, TriccNodeCalculateBase) and not isinstance(node, (TriccNodeNote, TriccNodeMoreInfo)):
236
+ # For regular nodes that get DataElements, use HIDEFIELD action
237
+ # Exclude TriccNodeNote and TriccNodeMoreInfo as they don't get DataElements
229
238
  program_rule_action = {
230
239
  "id": action_id,
231
240
  "programRuleActionType": "HIDEFIELD",
@@ -234,18 +243,45 @@ class DHIS2Strategy(BaseOutPutStrategy):
234
243
  },
235
244
  "programRule": {"id": rule_id}
236
245
  }
237
- self.program_rule_actions.append(program_rule_action)
246
+ self.program_rule_actions.append(program_rule_action)
238
247
 
239
248
  # Create program rule referencing the action
240
- condition = self.simplify_expression(f"!({relevance_str})") # Negate for hide when true
249
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
241
250
  condition = self.simplify_expression(condition)
242
251
  self.program_rules.append({
243
252
  "id": rule_id,
244
253
  "name": f"Hide `{self.get_export_name(node)}` when condition met",
245
- "description": f"Hide `{self.get_display(node)}` based on relevance",
254
+ "description": f"Hide `{self.get_display(node)[:128]}` based on relevance",
246
255
  "condition": condition,
247
256
  "programRuleActions": [{"id": action_id}]
248
257
  })
258
+
259
+ # Check if field should be mandatory based on 'required' attribute
260
+ if bool(getattr(node, 'required', False)):
261
+ # Create program rule for mandatory field using relevance function
262
+ mandatory_rule_id = self.generate_id(f"rule_{node.get_name()}_mandatory")
263
+ mandatory_action_id = self.generate_id(f"action_{mandatory_rule_id}")
264
+
265
+ mandatory_program_rule_action = {
266
+ "id": mandatory_action_id,
267
+ "programRuleActionType": "SETMANDATORYFIELD",
268
+ "dataElement": {
269
+ "id": self.generate_id(self.get_export_name(node))
270
+ },
271
+ "programRule": {"id": mandatory_rule_id}
272
+ }
273
+ self.program_rule_actions.append(mandatory_program_rule_action)
274
+
275
+ # Create program rule for mandatory field - use relevance condition if available
276
+ mandatory_condition = relevance_str if relevance_str and relevance_str != 'false' else "true"
277
+ self.program_rules.append({
278
+ "id": mandatory_rule_id,
279
+ "name": f"Make `{self.get_export_name(node)}` mandatory",
280
+ "description": f"Set `{self.get_display(node)[:128]}` as mandatory field",
281
+ "condition": mandatory_condition,
282
+ "programRuleActions": [{"id": mandatory_action_id}]
283
+ })
284
+
249
285
  return True
250
286
 
251
287
  def generate_data_element(self, node):
@@ -273,13 +309,12 @@ class DHIS2Strategy(BaseOutPutStrategy):
273
309
  "id": de_id,
274
310
  "name": self.get_export_name(node),
275
311
  "shortName": node.name[:50],
276
- "displayFormName":self.get_display(node),
312
+ "displayFormName": self.get_display(node),
277
313
  "formName": self.get_display(node),
278
314
  "valueType": value_type,
279
315
  "domainType": "TRACKER",
280
316
  "aggregationType": "NONE"
281
317
  }
282
-
283
318
  if issubclass(node.__class__, TriccNodeSelect) and not isinstance(node, TriccNodeSelectYesNo):
284
319
  data_element["optionSetValue"] = True
285
320
 
@@ -512,8 +547,16 @@ class DHIS2Strategy(BaseOutPutStrategy):
512
547
  return False
513
548
 
514
549
  if node not in processed_nodes:
550
+ logger.debug(f"generate_export processing node: {node.get_name()} type: {type(node)}")
551
+ # Special handling for TriccNodeNote - transform into section description
552
+ if isinstance(node, TriccNodeNote):
553
+ logger.info(f"Found TriccNodeNote: {node.get_name()}")
554
+ self.handle_note_as_section_description(node, processed_nodes, **kwargs)
555
+ elif isinstance(node, TriccNodeMoreInfo):
556
+ logger.info(f"Found TriccNodeMoreInfo: {node.get_name()}")
557
+ self.handle_note_as_section_description(node, processed_nodes, **kwargs)
515
558
  # Skip creating data elements for calculate nodes - they should only be program rule variables
516
- if not issubclass(node.__class__, TriccNodeCalculateBase):
559
+ elif not issubclass(node.__class__, TriccNodeCalculateBase):
517
560
  data_element = self.generate_data_element(node)
518
561
  if data_element:
519
562
  # Add to program stage
@@ -522,7 +565,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
522
565
  psde = {
523
566
  "id": psde_id,
524
567
  "dataElement": {"id": data_element["id"]},
525
- "compulsory": bool(getattr(node, 'required', False))
568
+ "compulsory": False
526
569
  }
527
570
  self.program_metadata["programStages"][-1]["programStageDataElements"].append(psde)
528
571
 
@@ -532,6 +575,115 @@ class DHIS2Strategy(BaseOutPutStrategy):
532
575
 
533
576
  return True
534
577
 
578
+ def handle_note_as_section_description(self, node, processed_nodes, **kwargs):
579
+ """Transform TriccNodeNote into section description and create hide logic with section duplication"""
580
+ logger.info(f"Processing note {node.get_name()} for section description")
581
+ if not self.current_section or self.current_section not in self.sections:
582
+ logger.warning(f"No current section found for note {node.get_name()}")
583
+ return
584
+
585
+ # 1. Duplicate the previous section with incremented ID (like XLS form group numbering)
586
+ original_section = self.sections[self.current_section]
587
+ section_name = f"{original_section['name']}_{node.name}"
588
+ duplicated_section_id = self.generate_id(section_name)
589
+ duplicated_section = {
590
+ "id": duplicated_section_id,
591
+ "name": section_name, # Use incremented naming pattern
592
+ "sortOrder": len(self.sections),
593
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
594
+ "dataElements": original_section.get("dataElements", []).copy(), # Copy data elements
595
+ "activity_ref": original_section.get("activity_ref")
596
+ }
597
+ self.sections[duplicated_section_id] = duplicated_section
598
+
599
+ # Add duplicated section to program stage
600
+ if self.program_metadata["programStages"]:
601
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": duplicated_section_id})
602
+
603
+ logger.info(f"Duplicated section {self.current_section} as {duplicated_section_id}")
604
+
605
+ # 2. Create a new section for the note/moreinfo with name and ID from the node
606
+ note_section_id = self.generate_id(self.get_export_name(node))
607
+ note_section_name = self.get_export_name(node)
608
+
609
+ note_section = {
610
+ "id": note_section_id,
611
+ "name": note_section_name,
612
+ "sortOrder": len(self.sections),
613
+ "programStage": {"id": self.program_metadata["programStages"][-1]["id"]},
614
+ "dataElements": [],
615
+ "activity_ref": original_section.get("activity_ref") # Same activity reference
616
+ }
617
+
618
+ # Set section description to the note's label
619
+ if hasattr(node, 'label') and node.label:
620
+ note_section["description"] = node.label.replace('\u00a0', ' ').strip()
621
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
622
+ elif hasattr(node, 'name') and node.name:
623
+ note_section["description"] = node.name.replace('\u00a0', ' ').strip()
624
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
625
+ else:
626
+ note_section["description"] = str(node.id)
627
+ logger.info(f"Set note section {note_section_id} description to: {note_section['description']}")
628
+
629
+ self.sections[note_section_id] = note_section
630
+
631
+ # Add note section to program stage
632
+ if self.program_metadata["programStages"]:
633
+ self.program_metadata["programStages"][-1]["programStageSections"].append({"id": note_section_id})
634
+
635
+ logger.info(f"Created note section {note_section_id} with name '{note_section_name}'")
636
+
637
+ # 3. Inject the duplicated section as the new current section
638
+ self.current_section = duplicated_section_id
639
+ logger.info(f"Set current section to duplicated section {duplicated_section_id}")
640
+
641
+ # Create hide logic for the note section based on combined relevance
642
+ combined_relevance = None
643
+
644
+ # Get parent activity relevance from the original section's activity_ref
645
+ parent_activity = original_section.get("activity_ref")
646
+ if parent_activity and hasattr(parent_activity, 'relevance') and parent_activity.relevance:
647
+ combined_relevance = parent_activity.relevance
648
+
649
+ # Combine with note relevance if it exists
650
+ if hasattr(node, 'relevance') and node.relevance:
651
+ if combined_relevance:
652
+ # Combine using AND operation
653
+ combined_relevance = TriccOperation(
654
+ TriccOperator.AND,
655
+ [combined_relevance, node.relevance]
656
+ )
657
+ else:
658
+ combined_relevance = node.relevance
659
+
660
+ # Create hide logic if there's relevance - applied to the note section
661
+ if combined_relevance:
662
+ relevance_str = self.convert_expression_to_string(not_clean(combined_relevance))
663
+ if relevance_str and relevance_str != 'false':
664
+ # Create program rule action for hiding the note section
665
+ rule_id = self.generate_id(f"rule_{node.get_name()}_note_hide_section")
666
+ action_id = self.generate_id(f"action_{rule_id}")
667
+
668
+ program_rule_action = {
669
+ "id": action_id,
670
+ "programRuleActionType": "HIDESECTION",
671
+ "activity_ref": parent_activity, # Use activity reference like other HIDESECTION actions
672
+ "programRule": {"id": rule_id},
673
+ }
674
+ self.program_rule_actions.append(program_rule_action)
675
+
676
+ # Create program rule referencing the action
677
+ condition = self.simplify_expression(f"({relevance_str})==false") # Negate for hide when true
678
+ condition = self.simplify_expression(condition)
679
+ self.program_rules.append({
680
+ "id": rule_id,
681
+ "name": f"Hide note section `{note_section_name}` based on relevance",
682
+ "description": f"Hide note section `{self.get_display(node)[:128]}` based on combined relevance",
683
+ "condition": condition,
684
+ "programRuleActions": [{"id": action_id}]
685
+ })
686
+
535
687
  def clean_section(self, program_stages_payload):
536
688
  """Clean sections by removing empty ones and merging sections with same activity_ref"""
537
689
  sections_to_remove = set()
@@ -542,11 +694,11 @@ class DHIS2Strategy(BaseOutPutStrategy):
542
694
  section_id = section["id"]
543
695
  activity_ref = section.get("activity_ref")
544
696
  # Remove empty sections
545
- if not section.get("dataElements"):
697
+ if not section.get("dataElements") and not section.get("description"):
546
698
  sections_to_remove.add(section_id)
547
699
 
548
700
  # Check for sections with same activity_ref
549
- elif activity_ref == prev_activity_ref:
701
+ elif activity_ref == prev_activity_ref and not section.get("description"):
550
702
  # Merge this section into the existing one
551
703
  existing_section = self.sections[prev_section_id]
552
704
 
@@ -645,6 +797,8 @@ class DHIS2Strategy(BaseOutPutStrategy):
645
797
  # Non-activity actions (HIDEFIELD) can be added directly
646
798
  program_rule_actions_payload.append(action)
647
799
 
800
+ # Filter out rules that reference non-existent actions
801
+ valid_action_ids = {action["id"] for action in program_rule_actions_payload}
648
802
  if self.program_rules:
649
803
  program_rules_payload = [
650
804
  {
@@ -652,6 +806,7 @@ class DHIS2Strategy(BaseOutPutStrategy):
652
806
  "program": {"id": self.program_metadata["id"]}
653
807
  }
654
808
  for rule in self.program_rules
809
+ if all(action_ref["id"] in valid_action_ids for action_ref in rule["programRuleActions"])
655
810
  ]
656
811
 
657
812
  if self.program_rule_variables:
@@ -736,15 +891,15 @@ class DHIS2Strategy(BaseOutPutStrategy):
736
891
  return f"#{{{node_id}}}"
737
892
  elif issubclass(r.__class__, TriccNodeCalculateBase):
738
893
  # Use variable name from concept_map
739
- node_id = self.get_export_name(r)
894
+ node_id = self.get_export_name(r)
740
895
  return f"#{{{node_id}}}"
741
896
  elif issubclass(r.__class__, TriccNodeInputModel):
742
897
  # Use variable name from concept_map
743
- node_id = self.get_export_name(r)
898
+ node_id = self.get_export_name(r)
744
899
  return f"#{{{node_id}}}"
745
900
  elif issubclass(r.__class__, TriccNodeBaseModel):
746
901
  # Use variable name from concept_map
747
- node_id = self.get_export_name(r)
902
+ node_id = self.get_export_name(r)
748
903
  return f"#{{{node_id}}}"
749
904
  else:
750
905
  raise NotImplementedError(f"This type of node {r.__class__.__name__} is not supported within an operation")
@@ -97,7 +97,7 @@ class FHIRStrategy(BaseOutPutStrategy):
97
97
  "type": self.map_tricc_type_to_fhir(node.tricc_type if hasattr(node, 'tricc_type') else 'text')
98
98
  }
99
99
  if hasattr(node, 'options') and node.options:
100
- item["answerOption"] = [{"valueString": opt.name} for opt in node.options]
100
+ item["answerOption"] = [{"valueString": opt.name} for opt in node.options.values()]
101
101
  self.questionnaires[segment]["item"].append(item)
102
102
  return True
103
103
 
@@ -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
@@ -1737,7 +1740,7 @@ PARENT_GROUP_PRIORITY = 6000
1737
1740
  ACTIVE_ACTIVITY_PRIORITY = 5000
1738
1741
  NON_START_ACTIVITY_PRIORITY = 4000
1739
1742
  ACTIVE_ACTIVITY_LOWER_PRIORITY = 3000
1740
- FLOW_CALCULATE_NODE_PRIORITY = 8000
1743
+ FLOW_CALCULATE_NODE_PRIORITY = 6500
1741
1744
  RHOMBUS_PRIORITY = 1000
1742
1745
  DEFAULT_PRIORITY = 2000
1743
1746
 
@@ -1755,6 +1758,11 @@ def reorder_node_list(node_list, group, processed_nodes):
1755
1758
  # Check for same group
1756
1759
  if group is not None and node_group and node_group.id == group.id:
1757
1760
  priority += SAME_GROUP_PRIORITY
1761
+ elif (
1762
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1763
+ isinstance(node, TriccNodeEnd)
1764
+ ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1765
+ priority += FLOW_CALCULATE_NODE_PRIORITY
1758
1766
  # Check for parent group
1759
1767
  elif hasattr(group, "group") and group.group and node_group and node_group.id == group.group.id:
1760
1768
  priority += PARENT_GROUP_PRIORITY
@@ -1765,7 +1773,10 @@ def reorder_node_list(node_list, group, processed_nodes):
1765
1773
  elif activity and isinstance(activity.root, TriccNodeActivityStart):
1766
1774
  priority += NON_START_ACTIVITY_PRIORITY
1767
1775
  # 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:
1776
+ elif (
1777
+ issubclass(node.__class__, TriccNodeDisplayCalculateBase) or
1778
+ isinstance(node, TriccNodeEnd)
1779
+ ) and not isinstance(node, TriccNodeActivityEnd) and hasattr(node, 'prev_nodes') and len(node.prev_nodes) > 0:
1769
1780
  priority += FLOW_CALCULATE_NODE_PRIORITY
1770
1781
  # Check for active activities (lower priority)
1771
1782
  elif activity and activity in active_activities:
@@ -2100,7 +2111,7 @@ def create_determine_diagnosis_activity(diags):
2100
2111
  )
2101
2112
  options = []
2102
2113
  for proposed in diags:
2103
- option = TriccNodeSelectOption(
2114
+ option = TriccNodeSelectOption(
2104
2115
  id=generate_id(proposed.name),
2105
2116
  name=proposed.name,
2106
2117
  label=proposed.label,
@@ -2108,6 +2119,7 @@ def create_determine_diagnosis_activity(diags):
2108
2119
  relevance=proposed.activity.applicability,
2109
2120
  select=f,
2110
2121
  )
2122
+ options.append(option)
2111
2123
  d = get_accept_diagnostic_node(proposed.name, proposed.label, proposed.severity, proposed.priority, activity)
2112
2124
  c = get_diagnostic_node(proposed.name, proposed.label, proposed.severity, proposed.priority, activity, option)
2113
2125
  diags_conf.append(d)
@@ -2134,7 +2146,6 @@ def create_determine_diagnosis_activity(diags):
2134
2146
  activity.nodes[wait2.id] = wait2
2135
2147
  # fallback
2136
2148
 
2137
-
2138
2149
  f.options = dict(zip(range(0, len(options)), options))
2139
2150
  activity.nodes[f.id] = f
2140
2151
  set_prev_next_node(f, end, edge_only=False)
@@ -2307,7 +2318,11 @@ def get_count_terms_details(prev_node, processed_nodes, get_overall_exp, negate=
2307
2318
  TriccOperator.CAST_NUMBER,
2308
2319
  [
2309
2320
  get_node_expression(
2310
- prev_node, processed_nodes=processed_nodes, get_overall_exp=get_overall_exp, is_prev=True, process=process
2321
+ prev_node,
2322
+ processed_nodes=processed_nodes,
2323
+ get_overall_exp=get_overall_exp,
2324
+ is_prev=True,
2325
+ process=process
2311
2326
  )
2312
2327
  ],
2313
2328
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tricc-oo
3
- Version: 1.6.7
3
+ Version: 1.6.9
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
@@ -8,9 +8,9 @@ tricc_oo/converters/codesystem_to_ocl.py,sha256=Fh7Vk73OsxljZKu1k6H9uzYwz334tpQT
8
8
  tricc_oo/converters/cql_to_operation.py,sha256=PUyV_YpUY98Ox0H_F_CN3UUf_I-BhFZVOcWWKTtwecM,14492
9
9
  tricc_oo/converters/datadictionnary.py,sha256=T2HLCBo4Am1p0kFqSH1r0PqbD8AC2IGuWkbvMvSCru0,3658
10
10
  tricc_oo/converters/drawio_type_map.py,sha256=UCPiGs7Lw0bigKScmZUnmOhACBz-FiDq92jHkI7RTSQ,9113
11
- tricc_oo/converters/tricc_to_xls_form.py,sha256=eIigjIuokOtnMRxUUGAdY5xfGX2zyTEvX5n7bOD8C00,3864
11
+ tricc_oo/converters/tricc_to_xls_form.py,sha256=wsWv4aA0QssY7ry9R7KsuuMzVfovj9fwE3i9AtCum0c,3842
12
12
  tricc_oo/converters/utils.py,sha256=JZrtrvvOfXwdkw49pKauzinOcauWwsy-CVcw36TjyLo,1684
13
- tricc_oo/converters/xml_to_tricc.py,sha256=T7oFEr56yNZ0empqlev6AaVrairl3evFciji-SeNmyA,39588
13
+ tricc_oo/converters/xml_to_tricc.py,sha256=yqnU5xg1SVoQ8vra0gOvIPwoh80pyTI332y9qJCvcRs,39654
14
14
  tricc_oo/converters/cql/cqlLexer.py,sha256=8HArbRphcrpnAG4uogJ2rHv4tc1WLzjN0B1uFeYILAc,49141
15
15
  tricc_oo/converters/cql/cqlListener.py,sha256=fA7-8DcS2Q69ckwjdg57-OfFHBxjTZFdoSKrtw7Hffc,57538
16
16
  tricc_oo/converters/cql/cqlParser.py,sha256=x3KdrwX9nwENSEJ5Ex7_l5NMnu3kWBO0uLdYu4moTq0,414745
@@ -26,27 +26,27 @@ tricc_oo/parsers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
26
26
  tricc_oo/parsers/xml.py,sha256=uzkb1y18MHfqVFmZqVh0sKT4cx6u0-NcAT_lV_gHBt8,4208
27
27
  tricc_oo/serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  tricc_oo/serializers/planuml.py,sha256=t57587-6L3aDncpHh58lS77Zft8yxDE9DPtXx2BeUSU,132
29
- tricc_oo/serializers/xls_form.py,sha256=m1IuzIRS2hjjl9mPPio4Dbq6ijg6CQvM7tGcQoMWOxg,22236
29
+ tricc_oo/serializers/xls_form.py,sha256=L0WF774zFt6PbdqncJGeyZzM9NiLq9NY5vO98yGTLhM,22133
30
30
  tricc_oo/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  tricc_oo/strategies/input/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  tricc_oo/strategies/input/base_input_strategy.py,sha256=BEODXS74na1QRRcJVQ4cxiD8F7uRqaLyhE3QzKpGVvk,3891
33
33
  tricc_oo/strategies/input/drawio.py,sha256=uXAUPhXOeg0Uk_BNqlCqFBW4cWNox4VfH559bj1fhC0,12767
34
- tricc_oo/strategies/output/base_output_strategy.py,sha256=M9UFR67-_CFoW681bPAeBS1OUGuFtmUbM_rltACI0hk,8798
35
- tricc_oo/strategies/output/dhis2_form.py,sha256=BmqFUaM2XFh-NtqxEbe6A6MBYPuBCEugR59pl9rhZw4,38298
36
- tricc_oo/strategies/output/fhir_form.py,sha256=hbL921pe1Doun4IQrJuZ_Sq2fCh98G3grYie5olC4uc,15740
34
+ tricc_oo/strategies/output/base_output_strategy.py,sha256=i9L5CVUqkEAMNyBsdHJ4xA7Nptr3myHr_fHHveDX1cU,8928
35
+ tricc_oo/strategies/output/dhis2_form.py,sha256=RFMUrB3TNJJi3kecBVThY6X1GF_TVk8XgXkye8fRkIc,47035
36
+ tricc_oo/strategies/output/fhir_form.py,sha256=yDDJqdkt4PvSDEy4kmgKM8V_o757CDlE7813atyyDHM,15749
37
37
  tricc_oo/strategies/output/html_form.py,sha256=qSleEZOMV_-Z04y-i-ucyd5rgAYWAyjPwMrw0IHtCRM,8604
38
- tricc_oo/strategies/output/openmrs_form.py,sha256=zAmDGMmZdIGNpil5MD-huiUvt_Dbhc2vt5qsGaCS2_k,29003
38
+ tricc_oo/strategies/output/openmrs_form.py,sha256=ne6TwAyhafR-WDs27QTKKFl85VD5sij_VEJtK6ZjOIE,28996
39
39
  tricc_oo/strategies/output/spice.py,sha256=QMeoismVC3PdbvwTK0PtUjWX9jl9780fbQIXn76fMXw,10761
40
- tricc_oo/strategies/output/xls_form.py,sha256=jXLw2r9surUC9uafn282gs_9oqEXueCg4gn0ql4Co-g,29562
40
+ tricc_oo/strategies/output/xls_form.py,sha256=_pNTND7n-55EjRphJ1hSVtRYa-UkXlmwpam2OKQ8o_w,30860
41
41
  tricc_oo/strategies/output/xlsform_cdss.py,sha256=X00Lt5MzV8TX14dR4dFI1MqllI5S1e13bKbeysWM9uA,17435
42
- tricc_oo/strategies/output/xlsform_cht.py,sha256=Kvs7kFk5LPaGP_vRJuOU-SgZRKO7aS7u52MWoc4mGdU,23023
42
+ tricc_oo/strategies/output/xlsform_cht.py,sha256=eKAc6LLDnvdZ5m8a2Vk6eAhvPbUvOfykgYr0ou3an9k,27164
43
43
  tricc_oo/strategies/output/xlsform_cht_hf.py,sha256=xm6SKirV3nMZvM2w54_zJcXAeAgAkq-EEqGEjnOWv6c,988
44
44
  tricc_oo/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- tricc_oo/visitors/tricc.py,sha256=sMnXsHc7fFDchzli6PqDNwYgXBikkHneZvzLXzbaySM,110132
45
+ tricc_oo/visitors/tricc.py,sha256=_UzAylSHuAhfXXPUBV_PJIhta5ihbJhLDe3GJAwxQzU,110598
46
46
  tricc_oo/visitors/utils.py,sha256=j83aAq5s5atXi3OC0jc_uJd54a8XrHHmizeeEbWZQJg,421
47
47
  tricc_oo/visitors/xform_pd.py,sha256=ryAnI3V9x3eTmJ2LNsUZfvl0_yfCqo6oBgeSu-WPqaE,9613
48
- tricc_oo-1.6.7.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
- tricc_oo-1.6.7.dist-info/METADATA,sha256=HlXKwa9g_2lxxkNeddXCGw6HF6P_cboZ65wHGGegzbo,8599
50
- tricc_oo-1.6.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- tricc_oo-1.6.7.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
- tricc_oo-1.6.7.dist-info/RECORD,,
48
+ tricc_oo-1.6.9.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
49
+ tricc_oo-1.6.9.dist-info/METADATA,sha256=qbIz4okrC-eeMx-8mZMt_--MUWzPcZmht_dhO11qovY,8599
50
+ tricc_oo-1.6.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
+ tricc_oo-1.6.9.dist-info/top_level.txt,sha256=NvbfMNAiy9m4b1unBsqpeOQWh4IgA1Xa33BtKA4abxk,15
52
+ tricc_oo-1.6.9.dist-info/RECORD,,