tricc-oo 1.5.13__py3-none-any.whl → 1.6.8__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.
Files changed (47) hide show
  1. tests/build.py +20 -28
  2. tests/test_build.py +260 -0
  3. tests/test_cql.py +48 -109
  4. tests/to_ocl.py +15 -17
  5. tricc_oo/__init__.py +0 -6
  6. tricc_oo/converters/codesystem_to_ocl.py +51 -40
  7. tricc_oo/converters/cql/cqlLexer.py +1 -0
  8. tricc_oo/converters/cql/cqlListener.py +1 -0
  9. tricc_oo/converters/cql/cqlParser.py +1 -0
  10. tricc_oo/converters/cql/cqlVisitor.py +1 -0
  11. tricc_oo/converters/cql_to_operation.py +129 -123
  12. tricc_oo/converters/datadictionnary.py +45 -54
  13. tricc_oo/converters/drawio_type_map.py +146 -65
  14. tricc_oo/converters/tricc_to_xls_form.py +58 -28
  15. tricc_oo/converters/utils.py +4 -4
  16. tricc_oo/converters/xml_to_tricc.py +296 -235
  17. tricc_oo/models/__init__.py +2 -1
  18. tricc_oo/models/base.py +333 -305
  19. tricc_oo/models/calculate.py +66 -51
  20. tricc_oo/models/lang.py +26 -27
  21. tricc_oo/models/ocl.py +146 -161
  22. tricc_oo/models/ordered_set.py +15 -19
  23. tricc_oo/models/tricc.py +149 -89
  24. tricc_oo/parsers/xml.py +15 -30
  25. tricc_oo/serializers/planuml.py +4 -6
  26. tricc_oo/serializers/xls_form.py +110 -153
  27. tricc_oo/strategies/input/base_input_strategy.py +28 -32
  28. tricc_oo/strategies/input/drawio.py +59 -71
  29. tricc_oo/strategies/output/base_output_strategy.py +151 -65
  30. tricc_oo/strategies/output/dhis2_form.py +908 -0
  31. tricc_oo/strategies/output/fhir_form.py +377 -0
  32. tricc_oo/strategies/output/html_form.py +224 -0
  33. tricc_oo/strategies/output/openmrs_form.py +694 -0
  34. tricc_oo/strategies/output/spice.py +106 -127
  35. tricc_oo/strategies/output/xls_form.py +322 -244
  36. tricc_oo/strategies/output/xlsform_cdss.py +627 -142
  37. tricc_oo/strategies/output/xlsform_cht.py +252 -125
  38. tricc_oo/strategies/output/xlsform_cht_hf.py +13 -24
  39. tricc_oo/visitors/tricc.py +1424 -1033
  40. tricc_oo/visitors/utils.py +16 -16
  41. tricc_oo/visitors/xform_pd.py +91 -89
  42. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/METADATA +128 -84
  43. tricc_oo-1.6.8.dist-info/RECORD +52 -0
  44. tricc_oo-1.6.8.dist-info/licenses/LICENSE +373 -0
  45. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/top_level.txt +0 -0
  46. tricc_oo-1.5.13.dist-info/RECORD +0 -46
  47. {tricc_oo-1.5.13.dist-info → tricc_oo-1.6.8.dist-info}/WHEEL +0 -0
tricc_oo/models/base.py CHANGED
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import logging
4
- import random
5
- import string
6
- from enum import Enum, auto
7
- from typing import Dict, ForwardRef, List, Optional, Union, Set, Annotated
4
+ from typing import Annotated, Dict, ForwardRef, List, Optional, Union
8
5
 
9
6
  from pydantic import BaseModel, StringConstraints
10
7
  from strenum import StrEnum
@@ -14,74 +11,57 @@ from tricc_oo.models.ordered_set import OrderedSet
14
11
 
15
12
  logger = logging.getLogger("default")
16
13
 
17
- Expression = Annotated[
18
- str,
19
- StringConstraints(pattern=r'^[^\\/\:]+$')
20
- ]
21
-
22
- triccId = Annotated[
23
- str,
24
- StringConstraints(pattern=r'^[^\\/\: ]+$')
25
- ]
14
+ Expression = Annotated[str, StringConstraints(pattern=r".+")]
26
15
 
27
- triccName = Annotated[
28
- str,
29
- StringConstraints(pattern=r'^[^\s]+( [^\s]+)*$')
30
- ]
31
-
32
- b64 = Annotated[
33
- str,
34
- StringConstraints(pattern=r'^[^-A-Za-z0-9+/=]|=[^=]|={3,}$')
35
- ]
16
+ triccId = Annotated[str, StringConstraints(pattern=r"^[^\\/\: ]+$")]
36
17
 
18
+ triccName = Annotated[str, StringConstraints(pattern=r"^[^\s]+( [^\s]+)*$")]
37
19
 
38
- TriccEdge = ForwardRef('TriccEdge')
39
- # data:page/id,UkO_xCL5ZjyshJO9Bexg
20
+ b64 = Annotated[str, StringConstraints(pattern=r"^[^-A-Za-z0-9+/=]|=[^=]|={3,}$")]
40
21
 
41
-
42
- ACTIVITY_END_NODE_FORMAT = "aend_{}"
43
22
  END_NODE_FORMAT = "end_{}"
44
23
 
45
24
 
46
25
  class TriccNodeType(StrEnum):
47
- #replace with auto ?
48
- note = 'note'
49
- calculate = 'calculate',
50
- output = 'output',
51
- select_multiple = 'select_multiple'
52
- select_one = 'select_one'
53
- select_yesno = 'select_one yesno'
54
- select_option = 'select_option'
55
- decimal = 'decimal'
56
- integer = 'integer'
57
- text = 'text'
58
- date = 'date'
59
- rhombus = 'rhombus' # fetch data
60
- goto = 'goto' #: start the linked activity within the target activity
61
- start = 'start' #: main start of the algo
62
- activity_start = 'activity_start' #: start of an activity (link in)
63
- link_in = 'link_in'
64
- link_out = 'link_out'
65
- count = 'count' #: count the number of valid input
66
- add = 'add' # add counts
67
- container_hint_media = 'container_hint_media' # DEPRECATED
68
- activity = 'activity'
69
- help = 'help-message'
70
- hint = 'hint-message'
71
- exclusive = 'not'
72
- end = 'end'
73
- activity_end = 'activity_end'
74
- edge = 'edge'
75
- page = 'container_page'
76
- not_available = 'not_available'
77
- quantity = 'quantity'
78
- bridge = 'bridge'
79
- wait = 'wait'
80
- operation = 'operation'
81
- context = 'context'
82
- diagnosis = 'diagnosis'
83
- proposed_diagnosis = 'proposed_diagnosis'
84
- input = 'input'
26
+ # replace with auto ?
27
+ note = "note"
28
+ calculate = ("calculate",)
29
+ output = ("output",)
30
+ select_multiple = "select_multiple"
31
+ select_one = "select_one"
32
+ select_yesno = "select_one yesno"
33
+ select_option = "select_option"
34
+ decimal = "decimal"
35
+ integer = "integer"
36
+ text = "text"
37
+ date = "date"
38
+ rhombus = "rhombus" # fetch data
39
+ goto = "goto" #: start the linked activity within the target activity
40
+ start = "start" #: main start of the algo
41
+ activity_start = "activity_start" #: start of an activity (link in)
42
+ link_in = "link_in"
43
+ link_out = "link_out"
44
+ count = "count" #: count the number of valid input
45
+ add = "add" # add counts
46
+ container_hint_media = "container_hint_media" # DEPRECATED
47
+ activity = "activity"
48
+ help = "help-message"
49
+ hint = "hint-message"
50
+ exclusive = "not"
51
+ end = "end"
52
+ activity_end = "activity_end"
53
+ edge = "edge"
54
+ page = "container_page"
55
+ not_available = "not_available"
56
+ quantity = "quantity"
57
+ bridge = "bridge"
58
+ wait = "wait"
59
+ operation = "operation"
60
+ context = "context"
61
+ diagnosis = "diagnosis"
62
+ proposed_diagnosis = "proposed_diagnosis"
63
+ input = "input"
64
+ remote_reference = "remote_reference"
85
65
 
86
66
  def __iter__(self):
87
67
  return iter(self.__members__.values())
@@ -90,7 +70,6 @@ class TriccNodeType(StrEnum):
90
70
  return next(iter(self))
91
71
 
92
72
 
93
-
94
73
  media_nodes = [
95
74
  TriccNodeType.note,
96
75
  TriccNodeType.select_multiple,
@@ -110,42 +89,42 @@ class TriccBaseModel(BaseModel):
110
89
  base_instance: Optional[TriccBaseModel] = None
111
90
  last: bool = None
112
91
  version: int = 1
92
+
113
93
  def get_datatype(self):
114
94
  return self.datatype or self.tricc_type
115
-
95
+
116
96
  def get_next_instance(self):
117
- if getattr(self, 'instances', None):
97
+ if getattr(self, "instances", None):
118
98
  return max(100, *[n.instance for n in self.instances.values()]) + 1
119
- if getattr(self, 'base_instance', None) and getattr(self.base_instance, 'instances', None):
99
+ if getattr(self, "base_instance", None) and getattr(self.base_instance, "instances", None):
120
100
  return max(100, *[n.instance for n in self.base_instance.instances.values()]) + 1
121
- return max(100,self.instance) + 1
122
-
101
+ return max(100, self.instance) + 1
102
+
123
103
  def to_dict(self):
124
- return {key: value for key, value in vars(self).items() if not key.startswith('_')}
104
+ return {key: value for key, value in vars(self).items() if not key.startswith("_")}
125
105
 
126
106
  def make_instance(self, nb_instance=None, **kwargs):
127
- if nb_instance == None:
107
+ if nb_instance is None:
128
108
  nb_instance = self.get_next_instance()
129
109
  instance = self.copy()
130
110
  attr_dict = self.to_dict()
131
111
  for attr, value in attr_dict.items():
132
- if not attr.startswith('_') and value is not None:
112
+ if not attr.startswith("_") and value is not None:
133
113
  try:
134
- if hasattr(value, 'copy'):
114
+ if hasattr(value, "copy"):
135
115
  setattr(instance, attr, value.copy())
136
116
  else:
137
117
  setattr(instance, attr, value)
138
118
  except Exception as e:
139
119
  logger.warning(f"Warning: Could not copy attribute {attr}: {e}")
140
-
120
+
141
121
  # change the id to avoid collision of name
142
122
  instance.id = generate_id(f"{self.id}{nb_instance}")
143
123
  instance.instance = int(nb_instance)
144
124
  instance.base_instance = self
145
- if hasattr(self, 'instances'):
125
+ if hasattr(self, "instances"):
146
126
  self.instances[nb_instance] = instance
147
-
148
-
127
+
149
128
  # assign the defualt group
150
129
  # if activity is not None and self.group == activity.base_instance:
151
130
  # instance.group = instance
@@ -166,16 +145,16 @@ class TriccBaseModel(BaseModel):
166
145
 
167
146
  def get_name(self):
168
147
  return self.id
169
-
148
+
170
149
  def __str__(self):
171
150
  return self.get_name()
172
-
151
+
173
152
  def __repr__(self):
174
153
  return f"{self.tricc_type}:{self.get_name()}({self.id})"
175
-
154
+
176
155
  def __init__(self, **data):
177
- if 'id' not in data:
178
- data['id'] = generate_id(str(data))
156
+ if "id" not in data:
157
+ data["id"] = generate_id(str(data))
179
158
  super().__init__(**data)
180
159
 
181
160
 
@@ -185,41 +164,45 @@ class TriccEdge(TriccBaseModel):
185
164
  source_external_id: triccId = None
186
165
  target: Union[triccId, TriccNodeBaseModel]
187
166
  target_external_id: triccId = None
188
- value: Optional[str] = None
189
-
167
+ value: Optional[str] = None
190
168
 
191
169
 
192
170
  class TriccGroup(TriccBaseModel):
193
171
  tricc_type: TriccNodeType = TriccNodeType.page
194
172
  group: Optional[TriccBaseModel] = None
173
+ activity: TriccBaseModel
195
174
  name: Optional[str] = None
196
- export_name:Optional[str] = None
197
- label: Optional[Union[str, Dict[str,str]]] = None
175
+ export_name: Optional[str] = None
176
+ label: Optional[Union[str, Dict[str, str]]] = None
198
177
  relevance: Optional[Union[Expression, TriccOperation]] = None
199
178
  path_len: int = 0
200
179
  prev_nodes: OrderedSet[TriccBaseModel] = OrderedSet()
180
+
201
181
  def __init__(self, **data):
202
182
  super().__init__(**data)
203
183
  if self.name is None:
204
184
  self.name = generate_id(str(data))
205
185
 
186
+ def gen_name(self):
187
+ if self.name is None:
188
+ self.name = get_rand_name(self.id)
189
+
206
190
  def get_name(self):
207
191
  result = str(super().get_name())
208
- name = getattr(self, 'name', None)
209
- label = getattr(self, 'label', None)
210
-
192
+ name = getattr(self, "name", None)
193
+ label = getattr(self, "label", None)
194
+
211
195
  if name:
212
196
  result = result + "::" + name
213
197
  if label:
214
- result = result + "::" + (
215
- next(iter(self.label.values())) if isinstance(self.label, Dict) else self.label
216
- )
198
+ result = result + "::" + (next(iter(self.label.values())) if isinstance(self.label, Dict) else self.label)
217
199
  if len(name) < 50:
218
200
  return result
219
201
  else:
220
- return result[:50]
202
+ return result[:50]
221
203
 
222
- FwTriccNodeBaseModel = ForwardRef('TriccNodeBaseModel')
204
+
205
+ FwTriccNodeBaseModel = ForwardRef("TriccNodeBaseModel")
223
206
 
224
207
 
225
208
  class TriccNodeBaseModel(TriccBaseModel):
@@ -227,44 +210,40 @@ class TriccNodeBaseModel(TriccBaseModel):
227
210
  group: Optional[Union[TriccGroup, FwTriccNodeBaseModel]] = None
228
211
  name: Optional[str] = None
229
212
  export_name: Optional[str] = None
230
- label: Optional[Union[str, Dict[str,str]]] = None
213
+ label: Optional[Union[str, Dict[str, str]]] = None
231
214
  next_nodes: OrderedSet[TriccNodeBaseModel] = OrderedSet()
232
215
  prev_nodes: OrderedSet[TriccNodeBaseModel] = OrderedSet()
233
216
  expression: Optional[Union[Expression, TriccOperation, TriccStatic]] = None # will be generated based on the input
234
217
  expression_inputs: List[Expression] = []
235
218
  activity: Optional[FwTriccNodeBaseModel] = None
236
- ref_def: Optional[Union[int,str]] = None# for medal creator
219
+ ref_def: Optional[Union[int, str]] = None # for medal creator
237
220
 
238
221
  class Config:
239
222
  use_enum_values = True # <--
240
-
223
+
241
224
  def __hash__(self):
242
- return hash(self.id )
225
+ return hash(self.id)
243
226
 
244
227
  # to be updated while processing because final expression will be possible to build$
245
- # #only the last time the script will go through the node (all prev node expression would be created
228
+ # #only the last time the script will go through the node (all prev node expression would be created
246
229
  def get_name(self):
247
- result = self.__class__.__name__[9:]# str(super().get_name())
248
- name = getattr(self, 'name', None)
249
- label = getattr(self, 'label', None)
250
-
230
+ result = self.__class__.__name__[9:] # str(super().get_name())
231
+ name = getattr(self, "name", None)
232
+ label = getattr(self, "label", None)
233
+
251
234
  if name:
252
235
  result += name
253
236
  if label:
254
- result += "::" + (
255
- next(iter(self.label.values())) if isinstance(self.label, Dict) else self.label
256
- )
237
+ result += "::" + (next(iter(self.label.values())) if isinstance(self.label, Dict) else self.label)
257
238
  if len(result) < 80:
258
239
  return result
259
240
  else:
260
- return result[:80]
261
-
262
-
241
+ return result[:80]
263
242
 
264
243
  def make_instance(self, instance_nb=None, activity=None):
265
244
  instance = super().make_instance(instance_nb)
266
245
  instance.group = activity
267
- if hasattr(self, 'activity') and activity is not None:
246
+ if hasattr(self, "activity") and activity is not None:
268
247
  instance.activity = activity
269
248
  next_nodes = OrderedSet()
270
249
  instance.next_nodes = next_nodes
@@ -272,31 +251,42 @@ class TriccNodeBaseModel(TriccBaseModel):
272
251
  instance.prev_nodes = prev_nodes
273
252
  expression_inputs = []
274
253
  instance.expression_inputs = expression_inputs
275
-
276
- for attr in ['expression', 'relevance', 'default', 'reference', 'expression_reference']:
254
+
255
+ for attr in [
256
+ "expression",
257
+ "relevance",
258
+ "default",
259
+ "reference",
260
+ "remote_reference",
261
+ "expression_reference",
262
+ ]:
277
263
  if getattr(self, attr, None):
278
- setattr(instance, attr, getattr(self, attr).copy())
279
-
264
+ setattr(instance, attr, getattr(self, attr))
265
+
280
266
  return instance
281
267
 
282
268
  def gen_name(self):
283
269
  if self.name is None:
284
270
  self.name = get_rand_name(self.id)
271
+
285
272
  def get_references(self):
286
273
  return OrderedSet()
287
274
 
275
+
288
276
  class TriccStatic(BaseModel):
289
- value: Union[str, float, int, bool]
290
- def __init__(self,value):
291
- super().__init__(value = value)
292
-
277
+ value: Union[str, float, int, bool, TriccNodeBaseModel]
278
+
279
+ def __init__(self, value):
280
+ super().__init__(value=value)
281
+
293
282
  def get_datatype(self):
294
- if str(type(self.value)) == 'bool':
295
- return 'boolean'
296
- elif str(self.value).isnumeric():
297
- return 'number'
283
+ if str(type(self.value)) == "bool":
284
+ return "boolean"
285
+ elif str(self.value).isnumeric():
286
+ return "number"
298
287
  else:
299
288
  return str(type(self.value))
289
+
300
290
  def __eq__(self, other):
301
291
  if isinstance(other, self.__class__):
302
292
  return self.value == other.value
@@ -309,20 +299,23 @@ class TriccStatic(BaseModel):
309
299
  def __hash__(self):
310
300
  hash_value = hash(self.value)
311
301
  return hash_value
302
+
312
303
  def get_name(self):
313
304
  return self.value
314
-
305
+
315
306
  def __str__(self):
316
307
  return str(self.value)
317
-
308
+
318
309
  def __repr__(self):
319
- return self.__class__.__name__+":"+str(type(self.value))+':' +str(self.value)
310
+ return self.__class__.__name__ + ":" + str(type(self.value)) + ":" + str(self.value)
320
311
 
321
312
  def get_references(self):
322
313
  return OrderedSet()
323
314
 
315
+
324
316
  class TriccReference(TriccStatic):
325
317
  value: str
318
+
326
319
  def __copy__(self):
327
320
  return type(self)(self.value)
328
321
 
@@ -333,57 +326,60 @@ class TriccReference(TriccStatic):
333
326
  return OrderedSet([self])
334
327
 
335
328
 
336
- class TriccOperator(StrEnum):
337
- AND = 'and' # and between left and rights
338
- ADD_OR = 'and_or' # left and one of the righs
339
- OR = 'or' # or between left and rights
340
- NATIVE = 'native' #default left is native expression
341
- ISTRUE = 'istrue' # left is right
342
- ISNOTTRUE = 'isnottrue'
343
- ISFALSE = 'isfalse' # left is false
344
- ISNOTFALSE = 'isnotfalse' # left is false
345
- SELECTED = 'selected' # right must be la select and one or several options
346
- MORE_OR_EQUAL = 'more_or_equal'
347
- LESS_OR_EQUAL = 'less_or_equal'
348
- EQUAL = 'equal'
349
- MORE = 'more'
350
- NOTEQUAL = 'not_equal'
351
- BETWEEN = 'between'
352
- LESS = 'less'
353
- CONTAINS = 'contains' # ref, txt Does CONTAINS make sense, like Select with wildcard
354
- NOTEXISTS = 'notexists'
355
- EXISTS = 'exists'
356
- NOT = 'not'
357
- ISNULL = 'isnull'
358
- ISNOTNULL= 'isnotnull'
359
-
360
- CASE = 'case' # ref (equal value, res), (equal value,res)
361
- IFS = 'ifs' #(cond, res), (cond,res)
362
- IF = 'if' # cond val_true, val_false
363
-
329
+ class TriccOperator(StrEnum):
330
+ AND = "and" # and between left and rights
331
+ ADD_OR = "and_or" # left and one of the righs
332
+ # ADD_STRING: 'add_string'
333
+ OR = "or" # or between left and rights
334
+ NATIVE = "native" # default left is native expression
335
+ ISTRUE = "istrue" # left is right
336
+ ISNOTTRUE = "isnottrue"
337
+ ISFALSE = "isfalse" # left is false
338
+ ISNOTFALSE = "isnotfalse" # left is false
339
+ SELECTED = "selected" # right must be la select and one or several options
340
+ MORE_OR_EQUAL = "more_or_equal"
341
+ LESS_OR_EQUAL = "less_or_equal"
342
+ EQUAL = "equal"
343
+ MORE = "more"
344
+ NOTEQUAL = "not_equal"
345
+ BETWEEN = "between"
346
+ LESS = "less"
347
+ CONTAINS = "contains" # ref, txt Does CONTAINS make sense, like Select with wildcard
348
+ NOTEXISTS = "notexists"
349
+ EXISTS = "exists"
350
+ NOT = "not"
351
+ ISNULL = "isnull"
352
+ ISNOTNULL = "isnotnull"
353
+ ROUND = "round"
354
+
355
+ CASE = "case" # ref (equal value, res), (equal value,res)
356
+ IFS = "ifs" # (cond, res), (cond,res)
357
+ IF = "if" # cond val_true, val_false
358
+
364
359
  # CDSS Specific
365
- HAS_QUALIFIER = 'has_qualifier'
366
-
367
- ZSCORE = 'zscore' # left table_name, right Y, gender give Z
368
- IZSCORE = 'izscore' #left table_name, right Z, gender give Y
369
- AGE_DAY = 'age_day' # age from dob
370
- AGE_MONTH = 'age_month' # age from dob
371
- AGE_YEAR = 'age_year' # age from dob
372
- DIVIDED = 'divided'
373
- MULTIPLIED = 'multiplied'
374
- PLUS = 'plus'
375
- MINUS = 'minus'
376
- MODULO = 'modulo'
377
- COUNT = 'count'
378
- CAST_NUMBER = 'cast_number'
379
- CAST_INTEGER = 'cast_integer'
380
- DRUG_DOSAGE = 'drug_dosage' # drug name, *param1 (ex: weight, age)
381
- COALESCE = 'coalesce'
382
- CAST_DATE = 'cast_date'
383
- PARENTHESIS = 'parenthesis'
384
- CONCATENATE = 'concatenate'
385
-
386
- RETURNS_BOOLEAN =[
360
+ HAS_QUALIFIER = "has_qualifier"
361
+ ZSCORE = "zscore" # left table_name, right Y, gender give Z
362
+ IZSCORE = "izscore" # left table_name, right Z, gender give Y
363
+ AGE_DAY = "age_day" # age from dob
364
+ AGE_MONTH = "age_month" # age from dob
365
+ AGE_YEAR = "age_year" # age from dob
366
+ DIVIDED = "divided"
367
+ MULTIPLIED = "multiplied"
368
+ PLUS = "plus"
369
+ MINUS = "minus"
370
+ MODULO = "modulo"
371
+ COUNT = "count"
372
+ CAST_NUMBER = "cast_number"
373
+ CAST_INTEGER = "cast_integer"
374
+ DRUG_DOSAGE = "drug_dosage" # drug name, *param1 (ex: weight, age)
375
+ COALESCE = "coalesce"
376
+ CAST_DATE = "cast_date"
377
+ PARENTHESIS = "parenthesis"
378
+ CONCATENATE = "concatenate"
379
+ DATETIME_TO_DECIMAL = "datetime_to_decimal"
380
+
381
+
382
+ RETURNS_BOOLEAN = [
387
383
  TriccOperator.ADD_OR,
388
384
  TriccOperator.AND,
389
385
  TriccOperator.OR,
@@ -404,7 +400,7 @@ RETURNS_BOOLEAN =[
404
400
  TriccOperator.LESS_OR_EQUAL,
405
401
  TriccOperator.EQUAL,
406
402
  TriccOperator.MORE,
407
- TriccOperator.LESS
403
+ TriccOperator.LESS,
408
404
  ]
409
405
 
410
406
  RETURNS_NUMBER = [
@@ -413,6 +409,8 @@ RETURNS_NUMBER = [
413
409
  TriccOperator.AGE_YEAR,
414
410
  TriccOperator.ZSCORE,
415
411
  TriccOperator.IZSCORE,
412
+ TriccOperator.ROUND,
413
+ TriccOperator.DATETIME_TO_DECIMAL,
416
414
  TriccOperator.PLUS,
417
415
  TriccOperator.MINUS,
418
416
  TriccOperator.DIVIDED,
@@ -420,86 +418,100 @@ RETURNS_NUMBER = [
420
418
  TriccOperator.COUNT,
421
419
  TriccOperator.MODULO,
422
420
  TriccOperator.CAST_NUMBER,
423
- TriccOperator.CAST_INTEGER
421
+ TriccOperator.CAST_INTEGER,
424
422
  ]
425
423
 
426
- RETURNS_DATE =[
427
- TriccOperator.CAST_DATE
428
- ]
424
+ RETURNS_DATE = [TriccOperator.CAST_DATE]
429
425
 
430
426
  OPERATION_LIST = {
431
- '>=': TriccOperator.MORE_OR_EQUAL,
432
- '<=': TriccOperator.LESS_OR_EQUAL,
433
- '==': TriccOperator.EQUAL,
434
- '!=': TriccOperator.NOTEQUAL,
435
- '=': TriccOperator.EQUAL,
436
- '>': TriccOperator.MORE,
437
- '<': TriccOperator.LESS
438
- }
427
+ ">=": TriccOperator.MORE_OR_EQUAL,
428
+ "<=": TriccOperator.LESS_OR_EQUAL,
429
+ "==": TriccOperator.EQUAL,
430
+ "!=": TriccOperator.NOTEQUAL,
431
+ "=": TriccOperator.EQUAL,
432
+ ">": TriccOperator.MORE,
433
+ "<": TriccOperator.LESS,
434
+ }
435
+
439
436
 
440
437
  class TriccOperation(BaseModel):
441
438
  tricc_type: TriccNodeType = TriccNodeType.operation
442
439
  operator: TriccOperator = TriccOperator.NATIVE
443
440
  reference: OrderedSet[
444
441
  Union[
445
- TriccStatic, TriccNodeBaseModel, TriccOperation, TriccReference, Expression,
446
- List[Union[TriccStatic, TriccNodeBaseModel, TriccOperation, TriccReference, Expression]]
442
+ TriccStatic,
443
+ TriccNodeBaseModel,
444
+ TriccOperation,
445
+ TriccReference,
446
+ Expression,
447
+ List[
448
+ Union[
449
+ TriccStatic,
450
+ TriccNodeBaseModel,
451
+ TriccOperation,
452
+ TriccReference,
453
+ Expression,
454
+ ]
455
+ ],
447
456
  ]
448
457
  ] = []
449
-
458
+
450
459
  def __str__(self):
451
460
  str_ref = map(str, self.reference)
452
461
  return f"{self.operator}({', '.join(map(str, str_ref))})"
453
-
462
+
454
463
  def __hash__(self):
455
464
  return hash(self.__repr__())
456
465
 
457
466
  def __repr__(self):
458
- return "TriccOperation:"+self.__str__()
459
-
467
+ str_ref = map(repr, self.reference)
468
+ return f"TriccOperation:{self.operator}({', '.join(map(str, str_ref))})"
469
+
460
470
  def __eq__(self, other):
461
471
  return self.__str__() == str(other)
462
-
472
+
463
473
  def __init__(self, operator, reference=[]):
464
474
  super().__init__(operator=operator, reference=reference)
465
-
475
+
466
476
  def get_datatype(self):
467
477
  if self.operator in RETURNS_BOOLEAN:
468
- return 'boolean'
478
+ return "boolean"
469
479
  elif self.operator in RETURNS_NUMBER:
470
- return 'number'
480
+ return "number"
471
481
  elif self.operator in RETURNS_DATE:
472
- return 'date'
482
+ return "date"
473
483
  elif self.operator == TriccOperator.CONCATENATE:
474
- return 'string'
484
+ return "string"
475
485
  elif self.operator == TriccOperator.PARENTHESIS:
476
486
  return self.get_reference_datatype(self.reference)
477
487
  elif self.operator == TriccOperator.IF:
478
488
  return self.get_reference_datatype(self.reference[1:])
479
- elif self.operator in ( TriccOperator.IFS, TriccOperator.CASE):
489
+ elif self.operator in (TriccOperator.IFS, TriccOperator.CASE):
480
490
  rtype = set()
481
491
  for rl in self.reference:
482
492
  rtype.add(self.get_reference_datatype(self.reference[-2:]))
483
- if len(rtype)>1:
484
- return 'mixed'
493
+ if len(rtype) > 1:
494
+ return "mixed"
485
495
  else:
486
- return rtype.pop()
487
-
496
+ return rtype.pop()
497
+ else:
498
+ return self.get_reference_datatype(self.reference)
499
+
488
500
  def get_reference_datatype(self, references):
489
501
  rtype = set()
490
502
  for r in references:
491
- if hasattr(r, 'get_datatype'):
503
+ if hasattr(r, "get_datatype"):
492
504
  rtype.add(r.get_datatype())
493
- elif hasattr(r, 'value'):
505
+ elif hasattr(r, "value"):
494
506
  return str(type(r.value))
495
507
  else:
496
508
  return str(type(r))
497
-
498
- if len(rtype)>1:
499
- return 'mixed'
509
+
510
+ if len(rtype) > 1:
511
+ return "mixed"
500
512
  else:
501
- return rtype.pop()
502
-
513
+ return rtype.pop()
514
+
503
515
  def get_references(self):
504
516
  predecessor = OrderedSet()
505
517
  if isinstance(self.reference, list):
@@ -508,7 +520,7 @@ class TriccOperation(BaseModel):
508
520
  else:
509
521
  raise NotImplementedError("cannot find predecessor of a str")
510
522
  return predecessor
511
-
523
+
512
524
  def _process_reference(self, reference, predecessor):
513
525
  if isinstance(reference, list):
514
526
  for e in reference:
@@ -522,10 +534,11 @@ class TriccOperation(BaseModel):
522
534
 
523
535
  def append(self, value):
524
536
  self.reference.append(value)
525
- def replace_node(self, old_node ,new_node):
537
+
538
+ def replace_node(self, old_node, new_node):
526
539
  if isinstance(self.reference, list):
527
540
  for key in [i for i, x in enumerate(self.reference)]:
528
- self.reference[key] = self._replace_reference(self.reference[key], new_node, old_node)
541
+ self.reference[key] = self._replace_reference(self.reference[key], new_node, old_node)
529
542
  elif self.reference is not None:
530
543
  raise NotImplementedError(f"cannot manage {self.reference.__class__}")
531
544
 
@@ -534,55 +547,64 @@ class TriccOperation(BaseModel):
534
547
  for key in [i for i, x in enumerate(reference)]:
535
548
  reference[key] = self._replace_reference(reference[key], new_node, old_node)
536
549
  if isinstance(reference, TriccOperation):
537
- reference.replace_node(old_node ,new_node)
550
+ reference.replace_node(old_node, new_node)
538
551
  elif issubclass(reference.__class__, (TriccNodeBaseModel, TriccReference)) and reference == old_node:
539
552
  reference = new_node
540
553
  # to cover the options
541
- if hasattr(reference, 'select') and hasattr(new_node, 'select') and issubclass(reference.select.__class__, TriccNodeBaseModel ) :
542
- self.replace_node(reference.select ,new_node.select)
554
+ if (
555
+ hasattr(reference, "select")
556
+ and hasattr(new_node, "select")
557
+ and issubclass(reference.select.__class__, TriccNodeBaseModel)
558
+ ):
559
+ self.replace_node(reference.select, new_node.select)
543
560
  return reference
544
-
561
+
545
562
  def __copy__(self, keep_node=False):
546
563
  # Create a new instance
547
564
  if keep_node:
548
565
  reference = [e for e in self.reference]
549
566
  else:
550
- reference = [e.copy() if isinstance(e, (TriccReference, TriccOperation)) else (TriccReference(e.name) if hasattr(e, 'name') else e) for e in self.reference]
551
-
552
-
553
- new_instance = type(self)(
554
- self.operator,
555
- reference
556
- )
567
+ reference = [
568
+ (
569
+ e.copy()
570
+ if isinstance(e, (TriccReference, TriccOperation))
571
+ else (TriccReference(e.name) if hasattr(e, "name") else e)
572
+ )
573
+ for e in self.reference
574
+ ]
575
+
576
+ new_instance = type(self)(self.operator, reference)
557
577
  # Copy attributes (shallow copy for mutable attributes)
558
-
578
+
559
579
  return new_instance
560
-
580
+
561
581
  def copy(self, keep_node=False):
562
582
  return self.__copy__(keep_node)
563
583
 
584
+
564
585
  # function that make multipat and
565
586
  # @param argv list of expression to join with and
566
587
  def clean_and_list(argv):
567
588
  for a in list(argv):
568
589
  if isinstance(a, TriccOperation) and a.operator == TriccOperator.AND:
569
590
  argv.remove(a)
570
- return clean_and_list([*argv,*a.reference])
571
- elif a == TriccStatic(True) or a == True:
591
+ return clean_and_list([*argv, *a.reference])
592
+ elif a == TriccStatic(True) or a is True:
572
593
  argv.remove(a)
573
- elif a == TriccStatic(False) :
594
+ elif a == TriccStatic(False):
574
595
  return [TriccStatic(False)]
575
-
596
+
576
597
  internal = list(set(argv))
577
598
  for a in internal:
578
- for b in internal[internal.index(a)+1:]:
599
+ for b in internal[internal.index(a) + 1:]:
579
600
  if not_clean(b) == a:
580
601
  return [TriccStatic(False)]
581
602
  return sorted(list(set(argv)), key=str)
582
-
603
+
604
+
583
605
  def not_clean(a):
584
606
  new_operator = None
585
- if a is None or isinstance(a, str) and a == '':
607
+ if a is None or isinstance(a, str) and a == "":
586
608
  return TriccStatic(False)
587
609
  elif isinstance(a, TriccStatic) and a == TriccStatic(False):
588
610
  return TriccStatic(True)
@@ -610,26 +632,16 @@ def not_clean(a):
610
632
  new_operator = TriccOperator.LESS
611
633
  elif isinstance(a, TriccOperation) and a.operator == TriccOperator.NOT:
612
634
  return a.reference[0]
613
-
635
+
614
636
  if new_operator:
615
- return TriccOperation(
616
- new_operator,
617
- a.reference
618
- )
619
-
637
+ return TriccOperation(new_operator, a.reference)
638
+
620
639
  elif not isinstance(a, TriccOperation) and issubclass(a.__class__, object):
621
- return TriccOperation(
622
- operator=TriccOperator.NOTEXISTS,
623
- reference=[a]
624
- )
640
+ return TriccOperation(operator=TriccOperator.NOTEXISTS, reference=[a])
625
641
  else:
626
- return TriccOperation(
627
- TriccOperator.NOT,
628
- [a]
629
- )
630
-
631
-
632
-
642
+ return TriccOperation(TriccOperator.NOT, [a])
643
+
644
+
633
645
  # function that generate remove unsure condition
634
646
  # @param list_or
635
647
  # @param and elm use upstream
@@ -641,44 +653,67 @@ def clean_or_list(list_or, elm_and=None):
641
653
  for a in list(list_or):
642
654
  if isinstance(a, TriccOperation) and a.operator == TriccOperator.OR:
643
655
  list_or.remove(a)
644
- return clean_or_list([*list_or,*a.reference])
645
- elif a == TriccStatic(False) or a == False or a == 0:
656
+ return clean_or_list([*list_or, *a.reference])
657
+ elif a == TriccStatic(False) or a is False or a == 0:
646
658
  list_or.remove(a)
647
- elif a == TriccStatic(True) or a == True or a == 1 or (elm_and is not None and not_clean(a) in list_or):
659
+ elif a == TriccStatic(True) or a is True or a == 1 or (elm_and is not None and not_clean(a) in list_or):
648
660
  return [TriccStatic(True)]
649
661
  # if there is x and not(X) in an OR list them the list is always true
650
- elif elm_and is not None and (not_clean(a) == elm_and or a == elm_and ):
662
+ elif elm_and is not None and (not_clean(a) == elm_and or a == elm_and):
651
663
  list_or.remove(a)
652
- internal = list(list_or)
664
+ internal = list(list_or)
653
665
  for a in internal:
654
- for b in internal[internal.index(a)+1:]:
666
+ for b in internal[internal.index(a) + 1:]:
655
667
  if not_clean(b) == a:
656
668
  return [TriccStatic(True)]
657
669
  if len(list_or) == 0:
658
670
  return []
659
671
 
660
- return sorted(list(set(list_or)), key=str)
672
+ return sorted(list(set(list_or)), key=repr)
673
+
661
674
 
662
675
  def and_join(argv):
663
- argv=clean_and_list(argv)
676
+ argv = clean_and_list(argv)
664
677
  if len(argv) == 0:
665
- return ''
678
+ return ""
666
679
  elif len(argv) == 1:
667
680
  return argv[0]
668
681
  else:
669
- return TriccOperation(
670
- TriccOperator.AND,
671
- argv
672
- )
682
+ return TriccOperation(TriccOperator.AND, argv)
683
+
684
+
685
+ def string_join(left: Union[str, TriccOperation], right: Union[str, TriccOperation]) -> TriccOperation:
686
+ """
687
+ Concatenates two arguments (strings or TriccOperation) into a TriccOperation with CONCATENATE operator.
688
+ If either argument is a TriccOperation with CONCATENATE operator, its operands are merged into the result.
689
+ """
690
+ # Initialize operands list for the new TriccOperation
691
+ operands: List[Union[str, TriccOperation]] = []
692
+
693
+ # Check if left is a TriccOperation with CONCATENATE
694
+ if isinstance(left, TriccOperation) and left.operator == TriccOperator.CONCATENATE:
695
+ operands.extend(left.reference) # Merge left's operands
696
+ else:
697
+ operands.append(left) # Add left as-is
698
+
699
+ # Check if right is a TriccOperation with CONCATENATE
700
+ if isinstance(right, TriccOperation) and right.operator == TriccOperator.CONCATENATE:
701
+ operands.extend(right.reference) # Merge right's operands
702
+ else:
703
+ operands.append(right) # Add right as-is
704
+
705
+ # Return a new TriccOperation with the merged operands
706
+ return TriccOperation(operator=TriccOperator.CONCATENATE, reference=operands)
707
+
673
708
 
674
709
  # function that make a 2 part and
675
710
  # @param left part
676
711
  # @param right part
677
712
  def simple_and_join(left, right):
678
- expression = None
713
+ pass
679
714
  # no term is considered as True
680
- left_issue = left is None or left == ''
681
- right_issue = right is None or right == ''
715
+ left_issue = left is None or left == ""
716
+ right_issue = right is None or right == ""
682
717
  left_neg = not_clean(left)
683
718
  right_neg = not_clean(right)
684
719
  if left_issue and right_issue:
@@ -686,61 +721,54 @@ def simple_and_join(left, right):
686
721
  elif left_neg == right or right_neg == left:
687
722
  return TriccStatic(False)
688
723
  elif left_issue:
689
- logger.debug('and with empty left term')
690
- return right
691
- elif left == '1' or left == 1 or left == TriccStatic(True) or left is True:
692
- return right
724
+ logger.debug("and with empty left term")
725
+ return right
726
+ elif left == "1" or left == 1 or left == TriccStatic(True) or left is True:
727
+ return right
693
728
  elif right_issue:
694
- logger.debug('and with empty right term')
695
- return left
696
- elif right == '1' or right == 1 or right == TriccStatic(True) or right is True:
697
- return left
729
+ logger.debug("and with empty right term")
730
+ return left
731
+ elif right == "1" or right == 1 or right == TriccStatic(True) or right is True:
732
+ return left
698
733
  else:
699
- return TriccOperation(
700
- TriccOperator.AND,
701
- [left, right]
702
- )
734
+ return TriccOperation(TriccOperator.AND, [left, right])
735
+
703
736
 
704
737
  def or_join(list_or, elm_and=None):
705
- cleaned_list = clean_or_list(set(list_or), elm_and)
738
+ cleaned_list = clean_or_list(set(list_or), elm_and)
706
739
  if len(cleaned_list) == 1:
707
740
  return cleaned_list[0]
708
- elif len(cleaned_list)>1:
709
- return TriccOperation(
710
- TriccOperator.OR,
711
- cleaned_list
712
- )
741
+ elif len(cleaned_list) > 1:
742
+ return TriccOperation(TriccOperator.OR, cleaned_list)
713
743
  else:
714
744
  logger.error("empty or list")
715
-
716
-
717
-
745
+
746
+
718
747
  # function that make a 2 part NAND
719
748
  # @param left part
720
749
  # @param right part
721
750
  def nand_join(left, right):
722
751
  # no term is considered as True
723
- left_issue = left is None or left == ''
724
- right_issue = right is None or right == ''
725
- left_neg = left == False or left == 0 or left == '0' or left == TriccStatic(False)
726
- right_neg = right == False or right == 0 or right == '0' or right == TriccStatic(False)
752
+ left_issue = left is None or left == ""
753
+ right_issue = right is None or right == ""
754
+ left_neg = left is False or left == 0 or left == "0" or left == TriccStatic(False)
755
+ right_neg = right is False or right == 0 or right == "0" or right == TriccStatic(False)
727
756
  if left_issue and right_issue:
728
757
  logger.critical("and with both terms empty")
729
758
  elif left_issue:
730
- logger.debug('and with empty left term')
731
- return not_clean(right)
732
- elif left == '1' or left == 1 or left == TriccStatic(True):
733
- return not_clean(right)
734
- elif right_issue :
735
- logger.debug('and with empty right term')
736
- return TriccStatic(False)
737
- elif right == '1' or right == 1 or left_neg or right == TriccStatic(True):
738
- return TriccStatic(False)
759
+ logger.debug("and with empty left term")
760
+ return not_clean(right)
761
+ elif left == "1" or left == 1 or left == TriccStatic(True):
762
+ return not_clean(right)
763
+ elif right_issue:
764
+ logger.debug("and with empty right term")
765
+ return TriccStatic(False)
766
+ elif right == "1" or right == 1 or left_neg or right == TriccStatic(True):
767
+ return TriccStatic(False)
739
768
  elif right_neg:
740
769
  return left
741
770
  else:
742
- return and_join([left, not_clean(right)])
771
+ return and_join([left, not_clean(right)])
743
772
 
744
773
 
745
774
  TriccGroup.update_forward_refs()
746
- TriccEdge.update_forward_refs()