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,183 @@ 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 get_feed_id_from_plugin_inst (self , plugin_inst : int ) -> int :
127+ """Get feed_id from a given plugin instance"""
128+ logger .info (f"Fetching feed id for plugin instance with ID: { plugin_inst } " )
129+ response = self .make_request ("GET" , f"/plugins/instances/{ plugin_inst } /" )
130+ for item in response :
131+ for field in item .get ("data" , []):
132+ if field .get ("name" ) == "feed_id" :
133+ return field .get ("value" )
134+ return - 1
135+
136+ def get_feed_details_from_id (self , feed_id : int ) -> dict :
137+ """Get feed details given a feed id"""
138+ feed_details = {}
139+
140+ logger .info (f"Getting feed details for ID: { feed_id } " )
141+ response = self .make_request ("GET" , f"/{ feed_id } /" )
142+ for item in response :
143+ for field in item .get ("data" , []):
144+ if field .get ("name" ) == "creation_date" :
145+ feed_details ["date" ] = field .get ("value" )
146+ if field .get ("name" ) == "name" :
147+ feed_details ["name" ] = field .get ("value" )
148+ if field .get ("name" ) == "owner_username" :
149+ feed_details ["owner" ] = field .get ("value" )
150+
151+ return feed_details
152+
153+ def post_workflow (self , pipeline_id : int , previous_id : int , params : list [dict ]) -> int :
117154 """
118155 Trigger a pipeline workflow in CUBE.
119156 """
120157 payload = {
121158 "previous_plugin_inst_id" : previous_id ,
122159 "nodes_info" : json .dumps (params )
123160 }
124- return self .post_request (f"/pipelines/{ pipeline_id } /workflows/" , json = payload )
161+ response = self .post_request (f"/pipelines/{ pipeline_id } /workflows/" , json = payload )
162+ for item in response :
163+ for field in item .get ("data" , []):
164+ if field .get ("name" ) == "id" :
165+ return field .get ("value" )
166+ return - 1
167+
168+ async def get_workflow_status (self , workflow_id : int ) -> dict :
169+ loop = asyncio .get_running_loop ()
170+ return await loop .run_in_executor (None , self ._get_workflow_status , workflow_id )
171+
172+ def _get_workflow_status (self , workflow_id : int ) -> dict :
173+ """
174+ 1. Get workflow details for a given workflow id.
175+ 2. Check for errored jobs
176+ 3. return total jobs (finished + errored + cancelled)
177+ """
178+ finished_jobs = 0
179+ errored_jobs = 0
180+ cancelled_jobs = 0
181+ created_jobs = 0
182+ waiting_jobs = 0
183+ scheduled_jobs = 0
184+ started_jobs = 0
185+ registering_jobs = 0
186+
187+ logger .info (f"Fetching workflow details for ID: { workflow_id } " )
188+ response = self .make_request ("GET" , f"/pipelines/workflows/{ workflow_id } /" )
189+ for item in response :
190+ for field in item .get ("data" , []):
191+ if field .get ("name" ) == "finished_jobs" :
192+ finished_jobs = field .get ("value" )
193+ if field .get ("name" ) == "errored_jobs" :
194+ errored_jobs = field .get ("value" )
195+ if field .get ("name" ) == "cancelled_jobs" :
196+ cancelled_jobs = field .get ("value" )
197+ if field .get ("name" ) == "created_jobs" :
198+ created_jobs = field .get ("value" )
199+ if field .get ("name" ) == "waiting_jobs" :
200+ waiting_jobs = field .get ("value" )
201+ if field .get ("name" ) == "scheduled_jobs" :
202+ scheduled_jobs = field .get ("value" )
203+ if field .get ("name" ) == "started_jobs" :
204+ started_jobs = field .get ("value" )
205+ if field .get ("name" ) == "registering_jobs" :
206+ registering_jobs = field .get ("value" )
207+
208+
209+ return {
210+ "finished_jobs" : finished_jobs ,
211+ "total_jobs" : finished_jobs + errored_jobs + cancelled_jobs + created_jobs + waiting_jobs + scheduled_jobs + started_jobs + registering_jobs ,
212+ "workflow_failed" : (errored_jobs > 0 )
213+ }
214+
215+ async def monitor_pipeline (self , workflow_id , total_jobs , pv_inst , rcpts , smtp , series_data ):
216+ while True :
217+ status = self ._get_workflow_status (workflow_id )
218+ if status ["workflow_failed" ]:
219+ logger .error ("Pipeline failed." )
220+ self .run_notification_plugin (pv_inst , "Pipeline failed with errors" , rcpts , smtp , series_data )
221+ break
222+ if status ["finished_jobs" ] >= total_jobs :
223+ logger .info ("Pipeline complete." )
224+ break
225+ if status ["total_jobs" ] < total_jobs :
226+ self .run_notification_plugin (pv_inst , "Nodes deleted in pipeline" , rcpts , smtp , series_data )
227+ break
228+ time .sleep (20 )
125229
126- def run_pipeline (self , pipeline_name : str , previous_inst : int , pipeline_params : dict ):
230+ def run_notification_plugin (self , pv_id : int , msg : str , rcpts : str , smtp : str , series_data : str ) -> int :
231+ """
232+ Run the pl-notification plugin.
233+ """
234+ feed_id = self .get_feed_id_from_plugin_inst (pv_id )
235+ feed_details = self .get_feed_details_from_id (feed_id )
236+ d_series = json .loads (series_data )
237+ email_content = (f"An error occurred while running anonymization pipeline on the following data: "
238+ f"\n Feed Name: { feed_details ['name' ]} "
239+ f"\n Date: { feed_details ['date' ]} "
240+ f"\n MRN: { d_series ['PatientID' ]} "
241+ f"\n StudyDate: { d_series ['StudyDate' ]} "
242+ f"\n Modality: { d_series ['Modality' ]} "
243+ f"\n SeriesDescription: { d_series ['SeriesDescription' ]} "
244+ f"\n Folder Name: { d_series ['Folder Name' ]} "
245+ f"\n \n Kindly login to ChRIS as *{ feed_details ['owner' ]} * to access the logs for more details." )
246+
247+ try :
248+ plugin_id = self ._get_plugin_id ({"name" : "pl-notification" , "version" : "0.1.0" })
249+ instance_id = self ._create_plugin_instance (plugin_id , {
250+ "previous_id" : pv_id ,
251+ "content" : email_content ,
252+ "title" : msg ,
253+ "rcpt" : rcpts ,
254+ "sender" : "noreply@fnndsc.org" ,
255+ "mail_server" : smtp
256+ })
257+ return int (instance_id )
258+ except Exception as ex :
259+ logger .error (f"Error occurred while creating notification instance { ex } " )
260+
261+ def _create_plugin_instance (self , plugin_id : str , params : dict ):
262+ """
263+ Create a plugin instance and return its ID.
264+ """
265+ response = self .post_request ( f"/plugins/{ plugin_id } /instances/" , json = params )
266+
267+ for item in response :
268+ for field in item .get ("data" , []):
269+ if field .get ("name" ) == "id" :
270+ return field .get ("value" )
271+
272+ raise RuntimeError ("Plugin instance could not be scheduled." )
273+
274+ def _get_plugin_id (self , params : dict ):
275+ """
276+ Fetch plugin ID by search parameters.
277+ """
278+ query_string = urlencode (params )
279+ response = self .make_request ("GET" , f"/plugins/search/?{ query_string } " )
280+
281+ for item in response :
282+ for field in item .get ("data" , []):
283+ if field .get ("name" ) == "id" :
284+ return field .get ("value" )
285+
286+ raise RuntimeError (f"No plugin found with matching criteria: { params } " )
287+
288+
289+ async def run_pipeline (self , pipeline_name : str , previous_inst : int , pipeline_params : dict , recipients : str , smtp_server : str , series_data : str ):
127290 """
128291 Full workflow to:
129292 1. Fetch pipeline ID
@@ -133,12 +296,19 @@ def run_pipeline(self, pipeline_name: str, previous_inst: int, pipeline_params:
133296 """
134297 try :
135298 pipeline_id = self .get_pipeline_id (pipeline_name )
299+ total_jobs = self .get_pipeline_total_pipings (pipeline_id )
136300 default_params = self .get_pipeline_parameters (pipeline_id )
137301 nodes_info = compute_workflow_nodes_info (default_params , include_all_defaults = True )
138302 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 )
303+ workflow_id = self .post_workflow (pipeline_id = pipeline_id , previous_id = previous_inst , params = updated_params )
304+ #self.run_notification_plugin(previous_inst)
305+
306+ # Start this in the background (not awaited)
307+ asyncio .create_task (self .monitor_pipeline (workflow_id , total_jobs , previous_inst , recipients , smtp_server , series_data ))
308+
140309 logger .info (f"Workflow posted successfully" )
141310 return {"status" : "Pipeline running" }
311+
142312 except Exception as ex :
143313 logger .error (f"Running pipeline failed due to: { ex } " )
144314 return {"status" : "Failed" , "error" : str (ex )}
0 commit comments