11"""Python script runtime implementation for executing and managing python scripts."""
22
3- import importlib .util
4- import inspect
5- import json
63import logging
7- import os
8- from dataclasses import asdict , is_dataclass
9- from typing import Any , Dict , Optional , Type , TypeVar , cast , get_type_hints
10-
11- from pydantic import BaseModel
4+ from typing import Any , Awaitable , Callable , Optional , TypeVar
125
136from ._contracts import (
147 UiPathBaseRuntime ,
158 UiPathErrorCategory ,
9+ UiPathRuntimeContext ,
1610 UiPathRuntimeError ,
1711 UiPathRuntimeResult ,
1812 UiPathRuntimeStatus ,
1913)
14+ from ._script_executor import ScriptExecutor
2015
2116logger = logging .getLogger (__name__ )
2217
2318T = TypeVar ("T" )
19+ R = TypeVar ("R" )
20+ AsyncFunc = Callable [[T ], Awaitable [R ]]
2421
2522
2623class UiPathRuntime (UiPathBaseRuntime ):
27- """Runtime for executing Python scripts."""
24+ def __init__ (self , context : UiPathRuntimeContext , executor : AsyncFunc [Any , Any ]):
25+ self .context = context
26+ self .executor = executor
27+
28+ async def cleanup (self ) -> None :
29+ """Cleanup runtime resources."""
30+ pass
31+
32+ async def validate (self ):
33+ """Validate runtime context."""
34+ pass
2835
2936 async def execute (self ) -> Optional [UiPathRuntimeResult ]:
3037 """Execute the Python script with the provided input and configuration.
@@ -35,15 +42,8 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
3542 Raises:
3643 UiPathRuntimeError: If execution fails
3744 """
38- await self .validate ()
39-
4045 try :
41- if self .context .entrypoint is None :
42- return None
43-
44- script_result = await self ._execute_python_script (
45- self .context .entrypoint , self .context .input_json
46- )
46+ script_result = await self .executor (self .context .input_json )
4747
4848 if self .context .job_id is None :
4949 logger .info (script_result )
@@ -65,248 +65,15 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
6565 UiPathErrorCategory .SYSTEM ,
6666 ) from e
6767
68- async def validate (self ) -> None :
69- """Validate runtime inputs."""
70- if not self .context .entrypoint :
71- raise UiPathRuntimeError (
72- "ENTRYPOINT_MISSING" ,
73- "No entrypoint specified" ,
74- "Please provide a path to a Python script." ,
75- UiPathErrorCategory .USER ,
76- )
77-
78- if not os .path .exists (self .context .entrypoint ):
79- raise UiPathRuntimeError (
80- "ENTRYPOINT_NOT_FOUND" ,
81- "Script not found" ,
82- f"Script not found at path { self .context .entrypoint } ." ,
83- UiPathErrorCategory .USER ,
84- )
85-
86- try :
87- if self .context .input :
88- self .context .input_json = json .loads (self .context .input )
89- if self .context .input_json is None :
90- self .context .input_json = {}
91- except json .JSONDecodeError as e :
92- raise UiPathRuntimeError (
93- "INPUT_INVALID_JSON" ,
94- "Invalid JSON input" ,
95- f"The input data is not valid JSON: { str (e )} " ,
96- UiPathErrorCategory .USER ,
97- ) from e
98-
99- async def cleanup (self ) -> None :
100- """Cleanup runtime resources."""
101- pass
102-
103- async def _execute_python_script (self , script_path : str , input_data : Any ) -> Any :
104- """Execute the Python script with the given input."""
105- spec = importlib .util .spec_from_file_location ("dynamic_module" , script_path )
106- if not spec or not spec .loader :
107- raise UiPathRuntimeError (
108- "IMPORT_ERROR" ,
109- "Module import failed" ,
110- f"Could not load spec for { script_path } " ,
111- UiPathErrorCategory .USER ,
112- )
113-
114- module = importlib .util .module_from_spec (spec )
115- try :
116- spec .loader .exec_module (module )
117- except Exception as e :
118- raise UiPathRuntimeError (
119- "MODULE_EXECUTION_ERROR" ,
120- "Module execution failed" ,
121- f"Error executing module: { str (e )} " ,
122- UiPathErrorCategory .USER ,
123- ) from e
124-
125- for func_name in ["main" , "run" , "execute" ]:
126- if hasattr (module , func_name ):
127- main_func = getattr (module , func_name )
128- sig = inspect .signature (main_func )
129- params = list (sig .parameters .values ())
130-
131- # Check if the function is asynchronous
132- is_async = inspect .iscoroutinefunction (main_func )
133-
134- # Case 1: No parameters
135- if not params :
136- try :
137- result = await main_func () if is_async else main_func ()
138- return (
139- self ._convert_from_class (result )
140- if result is not None
141- else {}
142- )
143- except Exception as e :
144- raise UiPathRuntimeError (
145- "FUNCTION_EXECUTION_ERROR" ,
146- f"Error executing { func_name } function" ,
147- f"Error: { str (e )} " ,
148- UiPathErrorCategory .USER ,
149- ) from e
150-
151- input_param = params [0 ]
152- input_type = input_param .annotation
153-
154- # Case 2: Class, dataclass, or Pydantic model parameter
155- if input_type != inspect .Parameter .empty and (
156- is_dataclass (input_type )
157- or self ._is_pydantic_model (input_type )
158- or hasattr (input_type , "__annotations__" )
159- ):
160- try :
161- valid_type = cast (Type [Any ], input_type )
162- typed_input = self ._convert_to_class (input_data , valid_type )
163- result = (
164- await main_func (typed_input )
165- if is_async
166- else main_func (typed_input )
167- )
168- return (
169- self ._convert_from_class (result )
170- if result is not None
171- else {}
172- )
173- except Exception as e :
174- raise UiPathRuntimeError (
175- "FUNCTION_EXECUTION_ERROR" ,
176- f"Error executing { func_name } function with typed input" ,
177- f"Error: { str (e )} " ,
178- UiPathErrorCategory .USER ,
179- ) from e
180-
181- # Case 3: Dict parameter
182- else :
183- try :
184- result = (
185- await main_func (input_data )
186- if is_async
187- else main_func (input_data )
188- )
189- return (
190- self ._convert_from_class (result )
191- if result is not None
192- else {}
193- )
194- except Exception as e :
195- raise UiPathRuntimeError (
196- "FUNCTION_EXECUTION_ERROR" ,
197- f"Error executing { func_name } function with dictionary input" ,
198- f"Error: { str (e )} " ,
199- UiPathErrorCategory .USER ,
200- ) from e
201-
202- raise UiPathRuntimeError (
203- "ENTRYPOINT_FUNCTION_MISSING" ,
204- "No entry function found" ,
205- f"No main function (main, run, or execute) found in { script_path } " ,
206- UiPathErrorCategory .USER ,
207- )
208-
209- def _convert_to_class (self , data : Dict [str , Any ], cls : Type [T ]) -> T :
210- """Convert a dictionary to either a dataclass, Pydantic model, or regular class instance."""
211- # Handle Pydantic models
212- try :
213- if inspect .isclass (cls ) and issubclass (cls , BaseModel ):
214- return cls .model_validate (data )
215- except TypeError :
216- # issubclass can raise TypeError if cls is not a class
217- pass
218-
219- # Handle dataclasses
220- if is_dataclass (cls ):
221- field_types = get_type_hints (cls )
222- converted_data = {}
22368
224- for field_name , field_type in field_types .items ():
225- if field_name not in data :
226- continue
227-
228- value = data [field_name ]
229- if (
230- is_dataclass (field_type )
231- or self ._is_pydantic_model (field_type )
232- or hasattr (field_type , "__annotations__" )
233- ) and isinstance (value , dict ):
234- typed_field = cast (Type [Any ], field_type )
235- value = self ._convert_to_class (value , typed_field )
236- converted_data [field_name ] = value
237-
238- return cls (** converted_data )
239-
240- # Handle regular classes
241- else :
242- sig = inspect .signature (cls .__init__ )
243- params = sig .parameters
244-
245- init_args = {}
246-
247- for param_name , param in params .items ():
248- if param_name == "self" :
249- continue
250-
251- if param_name in data :
252- value = data [param_name ]
253- param_type = (
254- param .annotation
255- if param .annotation != inspect .Parameter .empty
256- else Any
257- )
258-
259- if (
260- is_dataclass (param_type )
261- or self ._is_pydantic_model (param_type )
262- or hasattr (param_type , "__annotations__" )
263- ) and isinstance (value , dict ):
264- typed_param = cast (Type [Any ], param_type )
265- value = self ._convert_to_class (value , typed_param )
266-
267- init_args [param_name ] = value
268- elif param .default != inspect .Parameter .empty :
269- init_args [param_name ] = param .default
270-
271- return cls (** init_args )
272-
273- def _is_pydantic_model (self , cls : Type [Any ]) -> bool :
274- """Safely check if a class is a Pydantic model."""
275- try :
276- return inspect .isclass (cls ) and issubclass (cls , BaseModel )
277- except TypeError :
278- # issubclass can raise TypeError if cls is not a class
279- return False
280-
281- def _convert_from_class (self , obj : Any ) -> Dict [str , Any ]:
282- """Convert a class instance (dataclass, Pydantic model, or regular) to a dictionary."""
283- if obj is None :
284- return {}
285-
286- # Handle Pydantic models
287- if isinstance (obj , BaseModel ):
288- return obj .model_dump ()
69+ class UiPathScriptRuntime (UiPathRuntime ):
70+ """Runtime for executing Python scripts."""
28971
290- # Handle dataclasses
291- elif is_dataclass (obj ):
292- # Make sure obj is an instance, not a class
293- if isinstance (obj , type ):
294- return {}
295- return asdict (obj )
72+ def __init__ (self , context : UiPathRuntimeContext , entrypoint : str ):
73+ executor = ScriptExecutor (entrypoint )
74+ super ().__init__ (context , executor )
29675
297- # Handle regular classes
298- elif hasattr (obj , "__dict__" ):
299- result = {}
300- for key , value in obj .__dict__ .items ():
301- # Skip private attributes
302- if not key .startswith ("_" ):
303- if (
304- isinstance (value , BaseModel )
305- or hasattr (value , "__dict__" )
306- or is_dataclass (value )
307- ):
308- result [key ] = self ._convert_from_class (value )
309- else :
310- result [key ] = value
311- return result
312- return {} if obj is None else {str (type (obj ).__name__ ): str (obj )} # Fallback
76+ @classmethod
77+ def from_context (cls , context : UiPathRuntimeContext ):
78+ """Create runtime instance from context."""
79+ return UiPathScriptRuntime (context , context .entrypoint or "" )
0 commit comments