44from requests .exceptions import RequestException , Timeout , HTTPError
55from tenacity import retry , wait_exponential , stop_after_attempt , retry_if_exception_type
66from loguru import logger
7-
7+ import time
8+ import asyncio
9+ from urllib .parse import urlencode
10+ import pandas as pd
811
912def transform_plugin_data (nested_data_list : list [dict ]) -> list [dict ]:
1013 """Flatten nested plugin data into a list of dictionaries."""
@@ -107,23 +110,152 @@ def get_pipeline_id(self, name: str) -> int:
107110 return field .get ("value" )
108111 return - 1
109112
113+ def get_pipeline_total_pipings (self , pipeline_id : int ) -> int :
114+ """Get the total number of plugin pipings in the given pipeline."""
115+ logger .info (f"Fetching pipeline plugin piping list." )
116+ response = self .make_request ("GET" , f"/pipelines/{ pipeline_id } /pipings/?limit=100" )
117+ return len (response )
118+
119+
110120 def get_pipeline_parameters (self , pipeline_id : int ) -> list [dict ]:
111121 """Get default parameters for a pipeline."""
112122 logger .info (f"Fetching default parameters for pipeline with ID: { pipeline_id } " )
113123 response = self .make_request ("GET" , f"/pipelines/{ pipeline_id } /parameters/?limit=1000" )
114124 return transform_plugin_data (response )
115125
116- def post_workflow (self , pipeline_id : int , previous_id : int , params : list [dict ]) -> dict :
126+ def post_workflow (self , pipeline_id : int , previous_id : int , params : list [dict ]) -> int :
117127 """
118128 Trigger a pipeline workflow in CUBE.
119129 """
120130 payload = {
121131 "previous_plugin_inst_id" : previous_id ,
122132 "nodes_info" : json .dumps (params )
123133 }
124- return self .post_request (f"/pipelines/{ pipeline_id } /workflows/" , json = payload )
134+ response = self .post_request (f"/pipelines/{ pipeline_id } /workflows/" , json = payload )
135+ for item in response :
136+ for field in item .get ("data" , []):
137+ if field .get ("name" ) == "id" :
138+ return field .get ("value" )
139+ return - 1
140+
141+ async def get_workflow_status (self , workflow_id : int ) -> dict :
142+ loop = asyncio .get_running_loop ()
143+ return await loop .run_in_executor (None , self ._get_workflow_status , workflow_id )
144+
145+ def _get_workflow_status (self , workflow_id : int ) -> dict :
146+ """
147+ 1. Get workflow details for a given workflow id.
148+ 2. Check for errored jobs
149+ 3. return total jobs (finished + errored + cancelled)
150+ """
151+ finished_jobs = 0
152+ errored_jobs = 0
153+ cancelled_jobs = 0
154+ created_jobs = 0
155+ waiting_jobs = 0
156+ scheduled_jobs = 0
157+ started_jobs = 0
158+ registering_jobs = 0
159+
160+ logger .info (f"Fetching workflow details for ID: { workflow_id } " )
161+ response = self .make_request ("GET" , f"/pipelines/workflows/{ workflow_id } /" )
162+ for item in response :
163+ for field in item .get ("data" , []):
164+ if field .get ("name" ) == "finished_jobs" :
165+ finished_jobs = field .get ("value" )
166+ if field .get ("name" ) == "errored_jobs" :
167+ errored_jobs = field .get ("value" )
168+ if field .get ("name" ) == "cancelled_jobs" :
169+ cancelled_jobs = field .get ("value" )
170+ if field .get ("name" ) == "created_jobs" :
171+ created_jobs = field .get ("value" )
172+ if field .get ("name" ) == "waiting_jobs" :
173+ waiting_jobs = field .get ("value" )
174+ if field .get ("name" ) == "scheduled_jobs" :
175+ scheduled_jobs = field .get ("value" )
176+ if field .get ("name" ) == "started_jobs" :
177+ started_jobs = field .get ("value" )
178+ if field .get ("name" ) == "registering_jobs" :
179+ registering_jobs = field .get ("value" )
180+
181+
182+ return {
183+ "finished_jobs" : finished_jobs ,
184+ "total_jobs" : finished_jobs + errored_jobs + cancelled_jobs + created_jobs + waiting_jobs + scheduled_jobs + started_jobs + registering_jobs ,
185+ "workflow_failed" : (errored_jobs > 0 )
186+ }
187+
188+ async def monitor_pipeline (self , workflow_id , total_jobs , pv_inst , rcpts , smtp , series_data ):
189+ while True :
190+ status = self ._get_workflow_status (workflow_id )
191+ if status ["workflow_failed" ]:
192+ logger .error ("Pipeline failed." )
193+ self .run_notification_plugin (pv_inst , "Pipeline failed with errors" , rcpts , smtp , series_data )
194+ break
195+ if status ["finished_jobs" ] >= total_jobs :
196+ logger .info ("Pipeline complete." )
197+ break
198+ if status ["total_jobs" ] < total_jobs :
199+ self .run_notification_plugin (pv_inst , "Nodes deleted in pipeline" , rcpts , smtp , series_data )
200+ break
201+ time .sleep (20 )
202+
203+ def run_notification_plugin (self , pv_id : int , msg : str , rcpts : str , smtp : str , series_data : str ) -> int :
204+ """
205+ Run the pl-notification plugin.
206+ """
207+ d_series = json .loads (series_data )
208+ email_content = (f"An error occurred while running anonymization pipeline on the following data: "
209+ f"\n MRN: { d_series ['PatientID' ]} "
210+ f"\n StudyDate: { d_series ['StudyDate' ]} "
211+ f"\n Modality: { d_series ['Modality' ]} "
212+ f"\n SeriesDescription: { d_series ['SeriesDescription' ]} "
213+ f"\n Folder Name: { d_series ['Folder Name' ]} "
214+ f"\n \n Kindly login to ChRIS to access the logs for more details." )
215+
216+ try :
217+ plugin_id = self ._get_plugin_id ({"name" : "pl-notification" , "version" : "0.1.0" })
218+ instance_id = self ._create_plugin_instance (plugin_id , {
219+ "previous_id" : pv_id ,
220+ "content" : email_content ,
221+ "title" : "pipeline-error" ,
222+ "rcpt" : rcpts ,
223+ "sender" : "noreply@fnndsc.org" ,
224+ "mail_server" : smtp
225+ })
226+ return int (instance_id )
227+ except Exception as ex :
228+ logger .error (f"Error occurred while creating notification instance { ex } " )
229+
230+ def _create_plugin_instance (self , plugin_id : str , params : dict ):
231+ """
232+ Create a plugin instance and return its ID.
233+ """
234+ response = self .post_request ( f"/plugins/{ plugin_id } /instances/" , json = params )
235+
236+ for item in response :
237+ for field in item .get ("data" , []):
238+ if field .get ("name" ) == "id" :
239+ return field .get ("value" )
240+
241+ raise RuntimeError ("Plugin instance could not be scheduled." )
242+
243+ def _get_plugin_id (self , params : dict ):
244+ """
245+ Fetch plugin ID by search parameters.
246+ """
247+ query_string = urlencode (params )
248+ response = self .make_request ("GET" , f"/plugins/search/?{ query_string } " )
249+
250+ for item in response :
251+ for field in item .get ("data" , []):
252+ if field .get ("name" ) == "id" :
253+ return field .get ("value" )
254+
255+ raise RuntimeError (f"No plugin found with matching criteria: { params } " )
256+
125257
126- def run_pipeline (self , pipeline_name : str , previous_inst : int , pipeline_params : dict ):
258+ async def run_pipeline (self , pipeline_name : str , previous_inst : int , pipeline_params : dict , recipients : str , smtp_server : str , series_data : str ):
127259 """
128260 Full workflow to:
129261 1. Fetch pipeline ID
@@ -133,12 +265,19 @@ def run_pipeline(self, pipeline_name: str, previous_inst: int, pipeline_params:
133265 """
134266 try :
135267 pipeline_id = self .get_pipeline_id (pipeline_name )
268+ total_jobs = self .get_pipeline_total_pipings (pipeline_id )
136269 default_params = self .get_pipeline_parameters (pipeline_id )
137270 nodes_info = compute_workflow_nodes_info (default_params , include_all_defaults = True )
138271 updated_params = update_plugin_parameters (nodes_info , pipeline_params )
139- workflow = self .post_workflow (pipeline_id = pipeline_id , previous_id = previous_inst , params = updated_params )
272+ workflow_id = self .post_workflow (pipeline_id = pipeline_id , previous_id = previous_inst , params = updated_params )
273+ #self.run_notification_plugin(previous_inst)
274+
275+ # Start this in the background (not awaited)
276+ asyncio .create_task (self .monitor_pipeline (workflow_id , total_jobs , previous_inst , recipients , smtp_server , series_data ))
277+
140278 logger .info (f"Workflow posted successfully" )
141279 return {"status" : "Pipeline running" }
280+
142281 except Exception as ex :
143282 logger .error (f"Running pipeline failed due to: { ex } " )
144283 return {"status" : "Failed" , "error" : str (ex )}
0 commit comments