33"""
44
55from __future__ import annotations
6- from lxml import etree as ET
6+
7+ import sys
8+
9+ sys .modules ['_elementtree' ] = None
10+ import xml .etree .ElementTree as ET
711
812from typing import Optional
913import logging
2731logger : logging .Logger = logging .getLogger (__name__ )
2832
2933
30- def get_root (path : str ) -> ET ._Element :
34+ def get_root (path : str ) -> ET .Element :
3135 """Get flow root
3236
3337 Args:
@@ -37,7 +41,7 @@ def get_root(path: str) -> ET._Element:
3741 the root of the xml file
3842
3943 """
40- return ET .parse (path ).getroot ()
44+ return ET .parse (path , parser = parse_utils . LineNumberingParser () ).getroot ()
4145
4246
4347class Parser (FlowParser ):
@@ -59,7 +63,7 @@ class Parser(FlowParser):
5963
6064 def __init__ (self , root ):
6165 #: XMl root of a single flow
62- self .root : ET ._Element = root
66+ self .root : ET .Element = root
6367
6468 #: current filepath of flow
6569 self .flow_path : str | None = None
@@ -74,7 +78,7 @@ def __init__(self, root):
7478 self .flow_type : FlowType | None = None
7579
7680 #: frozen set of all elements that have a child of <name> and are thus flow globals
77- self .all_named_elems : frozenset [ET ._Element ] | None = None
81+ self .all_named_elems : frozenset [ET .Element ] | None = None
7882
7983 #: variables marked 'available for input', as a pair (flow_path, name)
8084 self .input_variables : frozenset [(str , str )] | None = None
@@ -100,7 +104,7 @@ def get_declared_run_mode(self) -> RunMode:
100104 def get_filename (self ) -> str :
101105 return self .flow_path
102106
103- def get_root (self ) -> ET ._Element :
107+ def get_root (self ) -> ET .Element :
104108 return self .root
105109
106110 def get_literal_var (self ) -> VariableType :
@@ -124,41 +128,47 @@ def get_flow_type(self) -> FlowType:
124128 # Process Builder
125129 # no <start> but <startElementReference>
126130 res = get_by_tag (self .root , 'startElementReference' )
127- if len (res ) > 0 :
128- flow_type = FlowType .ProcessBuilder
129-
130- else :
131+ if len (res ) == 0 :
131132 res = get_by_tag (self .root , 'start' )
132- assert len (res ) != 0
133- start = res [0 ]
133+ if len (res ) == 0 :
134+ # this is an old format record trigger flow
135+ self .flow_type = FlowType .RecordTrigger
136+ return FlowType .RecordTrigger
137+ start = res [0 ]
134138
135- # Trigger, record
136- # <start> has a child <triggerType>
137- child = get_by_tag (start , 'triggerType' )
138- if len (child ) > 0 :
139- flow_type = FlowType .RecordTrigger
139+ # Trigger, record
140+ # <start> has a child <triggerType>
141+ child = get_by_tag (start , 'triggerType' )
142+ if len (child ) > 0 :
143+ flow_type = FlowType .RecordTrigger
140144
141- elif len (get_by_tag (start , 'schedule' )) > 0 :
142- flow_type = FlowType .Scheduled
145+ elif len (get_by_tag (start , 'schedule' )) > 0 :
146+ flow_type = FlowType .Scheduled
143147
144- else :
145- # We couldn't determine flow type by looking at
146- # <start> elem, so now look at processType elem
147- pt = get_by_tag (self .root , 'processType' )
148- if len (pt ) > 0 :
149- pt = pt [0 ].text
150-
151- # Screen
152- # <processType>Flow and start does not have trigger or schedule
153- if pt == 'Flow' or len (get_by_tag (self .root , 'screens' )) > 0 :
154- flow_type = FlowType .Screen
155-
156- # AutoLaunched
157- # Some teams have their own names, e.g. FooAutolaunchedFlow
158- # Notice this messes up capitalization from normal 'AutoLaunchedFlow'
159- # there are also recommendation strategies, etc.
160- else :
161- flow_type = FlowType .AutoLaunched
148+ else :
149+ # We couldn't determine flow type by looking at
150+ # <start> elem, so now look at processType elem
151+ pt = get_by_tag (self .root , 'processType' )
152+ if len (pt ) > 0 :
153+ pt = pt [0 ].text
154+
155+ # Screen
156+ # <processType>Flow and start does not have trigger or schedule
157+ if pt == 'Flow' or len (get_by_tag (self .root , 'screens' )) > 0 :
158+ flow_type = FlowType .Screen
159+
160+ elif pt .lower () == 'workflow' :
161+ flow_type = FlowType .Workflow
162+
163+ elif pt .lower () == 'invocableprocess' :
164+ flow_type = FlowType .InvocableProcess
165+
166+ # AutoLaunched
167+ # Some teams have their own names, e.g. FooAutolaunchedFlow
168+ # Notice this messes up capitalization from normal 'AutoLaunchedFlow'
169+ # there are also recommendation strategies, etc.
170+ else :
171+ flow_type = FlowType .AutoLaunched
162172
163173 if flow_type is not None :
164174 self .flow_type = flow_type
@@ -174,7 +184,8 @@ def resolve_by_name(self, name: str, path: str | None = None,
174184
175185 "Account_var.Name" --> ("Account_var", "Name", VariableType)
176186 "account_var" --> (account_var, None, VariableType).
177- (my_subflow.account.Name) --> (my_subflow.account, Name, VarType)
187+ (my_subflow.account.Name) --> (my_subflow.account, Name, VariableType)
188+ (my_action_call.account) --> (my_action_call.account, None, VariableType)
178189
179190 Args:
180191 name: raw name as it is used in the flow xml file (e.g. foo.bar.baz)
@@ -206,6 +217,9 @@ def resolve_by_name(self, name: str, path: str | None = None,
206217 if spl_len == 1 :
207218 logger .warning (f"RESOLUTION ERROR { name } " )
208219 if strict is False :
220+ # 'strict' = False means that any unknown variable name
221+ # is assumed to be a string literal that is hardcoded into
222+ # the flows runtime and so not declared in flow xml file
209223 return name , None , self .literal_var
210224 else :
211225 return None
@@ -231,7 +245,7 @@ def resolve_by_name(self, name: str, path: str | None = None,
231245
232246 @classmethod
233247 def from_file (cls , filepath : str , old_parser : Parser = None ) -> Parser :
234- root = ET .parse (filepath ).getroot ()
248+ root = ET .parse (filepath , parser = parse_utils . LineNumberingParser () ).getroot ()
235249 parser = Parser (root )
236250 parser .flow_path = filepath
237251 parser .update (old_parser = old_parser )
@@ -300,10 +314,10 @@ def get_input_variables(self, path: str | None = None) -> {(str, str)}:
300314 path = self .flow_path
301315 return {(x , y ) for (x , y ) in self .input_variables if x == path }
302316
303- def get_input_field_elems (self ) -> set [ET ._Element ] | None :
317+ def get_input_field_elems (self ) -> set [ET .Element ] | None :
304318 return parse_utils .get_input_fields (self .root )
305319
306- def get_input_output_elems (self ) -> {str : set [ET ._Element ]}:
320+ def get_input_output_elems (self ) -> {str : set [ET .Element ]}:
307321 """
308322 Returns::
309323 {"input": input variable elements,
@@ -325,7 +339,7 @@ def get_input_output_elems(self) -> {str: set[ET._Element]}:
325339 "output" : output_accum
326340 }
327341
328- def get_by_name (self , name_to_match : str , scope : ET ._Element | None = None ) -> ET ._Element | None :
342+ def get_by_name (self , name_to_match : str , scope : ET .Element | None = None ) -> ET .Element | None :
329343 """returns the first elem with the given name that is a child of the scope element"""
330344 if name_to_match == '*' :
331345 return self .get_start_elem ()
@@ -356,9 +370,17 @@ def get_run_mode(self) -> RunMode:
356370
357371 """
358372 flow_type = self .get_flow_type ()
359- if flow_type is not FlowType .Screen and flow_type is not FlowType .AutoLaunched :
373+
374+ if flow_type is FlowType .InvocableProcess :
375+ # always runs in user mode
376+ return RunMode .DefaultMode
377+
378+ if flow_type in [FlowType .Workflow , FlowType .RecordTrigger , FlowType .Scheduled , FlowType .ProcessBuilder ]:
379+ # always runs in system mode
360380 return RunMode .SystemModeWithoutSharing
361381
382+ # for screen and other autolaunched, check if there is a declaration
383+ # otherwise go with default
362384 elems = get_by_tag (self .root , 'runInMode' )
363385 if len (elems ) == 0 :
364386 return RunMode .DefaultMode
@@ -368,48 +390,48 @@ def get_run_mode(self) -> RunMode:
368390 def get_api_version (self ) -> str :
369391 return get_by_tag (self .root , 'apiVersion' )[0 ].text
370392
371- def get_all_traversable_flow_elements (self ) -> [ET ._Element ]:
393+ def get_all_traversable_flow_elements (self ) -> [ET .Element ]:
372394 """ ignore start"""
373395 return [child for child in self .root if
374396 get_tag (child ) in ['actionCalls' , 'assignments' , 'decisions' , 'loops' ,
375397 'recordLookups' , 'recordUpdates' ,
376398 'collectionProcessors' , 'recordDeletes' , 'recordCreates' , 'screens' , 'subflows' ,
377399 'waits' , 'recordRollbacks' ]]
378400
379- def get_all_variable_elems (self ) -> [ET ._Element ] or None :
401+ def get_all_variable_elems (self ) -> [ET .Element ] or None :
380402 elems = get_by_tag (self .root , 'variables' )
381403 if len (elems ) == 0 :
382404 return None
383405 else :
384406 return elems
385407
386- def get_templates (self ) -> [ET ._Element ]:
408+ def get_templates (self ) -> [ET .Element ]:
387409 """Grabs all template elements.
388410 Returns empty list if none found
389411 """
390412 templates = get_by_tag (self .root , 'textTemplates' )
391413 return templates
392414
393- def get_formulas (self ) -> [ET ._Element ]:
415+ def get_formulas (self ) -> [ET .Element ]:
394416 """Grabs all formula elements.
395417 Returns empty list if none found
396418 """
397419 formulas = get_by_tag (self .root , 'formulas' )
398420 return formulas
399421
400- def get_choices (self ) -> [ET ._Element ]:
422+ def get_choices (self ) -> [ET .Element ]:
401423 choices = get_by_tag (self .root , 'choices' )
402424 return choices
403425
404- def get_dynamic_choice_sets (self ) -> [ET ._Element ]:
426+ def get_dynamic_choice_sets (self ) -> [ET .Element ]:
405427 dcc = get_by_tag (self .root , 'dynamicChoiceSets' )
406428 return dcc
407429
408- def get_constants (self ) -> [ET ._Element ]:
430+ def get_constants (self ) -> [ET .Element ]:
409431 constants = get_by_tag (self .root , 'constants' )
410432 return constants
411433
412- def get_start_elem (self ) -> ET ._Element :
434+ def get_start_elem (self ) -> ET .Element :
413435 """Get first element of flow
414436
415437 Returns:
@@ -424,10 +446,16 @@ def get_start_elem(self) -> ET._Element:
424446 elif len (res2 ) == 1 :
425447 return self .get_by_name (res2 [0 ].text )
426448
449+ # Put in provision for older flows that are missing start elements but have only
450+ # a single crud element
451+ candidates = get_by_tag (self .root , 'recordUpdates' )
452+ if len (candidates ) == 1 :
453+ return candidates [0 ]
454+
427455 else :
428456 raise RuntimeError ("Currently only flows with a single 'start' or 'startElementReference' can be scanned" )
429457
430- def get_all_indirect_tuples (self ) -> list [tuple [str , ET ._Element ]]:
458+ def get_all_indirect_tuples (self ) -> list [tuple [str , ET .Element ]]:
431459 """returns a list of tuples of all indirect references, e.g.
432460 str, elem, where str influences elem.
433461 The elem is a formula or template element and
@@ -483,13 +511,13 @@ def __get_type(self, name: str, path: str | None = None, strict: bool = False) -
483511
484512 else :
485513 logger .info (f"Auto-resolving { name } in file { self .flow_path } " )
486- if strict is False :
514+ if strict is True :
487515 return self .literal_var
488516 else :
489517 return None
490518
491519
492- def build_vartype_from_elem (elem : ET ._Element ) -> VariableType | None :
520+ def build_vartype_from_elem (elem : ET .Element ) -> VariableType | None :
493521 """Build VariableType from XML Element
494522
495523 The purpose of this function is to assign types to named
@@ -538,6 +566,15 @@ def build_vartype_from_elem(elem: ET._Element) -> VariableType | None:
538566 object_name = parse_utils .get_obj_name (elem ),
539567 is_optional = nulls_provided is not None and nulls_provided is False )
540568
569+ if tag == 'actionCalls' :
570+ is_ = parse_utils .is_auto_store (elem )
571+ if is_ is True :
572+ reference = ReferenceType .ActionCallReference
573+ # TODO: see if we can get datatype info from return value
574+
575+ return VariableType (tag = tag , datatype = DataType .StringValue ,
576+ reference = reference , is_optional = False )
577+
541578 if tag == 'recordCreates' :
542579 # Todo: get collection parsing correct, look if record being created is itself
543580 # a collection element - do examples of bulkified versions of commands.
@@ -640,7 +677,7 @@ def build_vartype_from_elem(elem: ET._Element) -> VariableType | None:
640677 return None
641678
642679
643- def _get_global_flow_data (flow_path , root : ET ._Element ) -> ([ET ._Element ], {str : VariableType }):
680+ def _get_global_flow_data (flow_path , root : ET .Element ) -> ([ET .Element ], {str : VariableType }):
644681 all_named = get_named_elems (root )
645682
646683 # all named cannot be None, each flow must have at least one named element.
@@ -655,15 +692,15 @@ def _get_global_flow_data(flow_path, root: ET._Element) -> ([ET._Element], {str:
655692 try :
656693 var = build_vartype_from_elem (x )
657694 except Exception :
658- logger .error (f"ERROR parsing element { ET .tounicode ( x )} " )
695+ logger .error (f"ERROR parsing element { ET .tostring ( x , encoding = 'unicode' )} " )
659696 continue
660697 if var is not None :
661698 vars_ [(flow_path , name_dict [x ])] = var
662699
663- if var .is_input and var . is_input is True :
700+ if var .is_input is True :
664701 inputs .append ((flow_path , name_dict [x ]))
665702
666- if var .is_output and var . is_output is True :
703+ if var .is_output is True :
667704 outputs .append ((flow_path , name_dict [x ]))
668705
669706 return all_named , vars_ , frozenset (inputs ), frozenset (outputs )
0 commit comments