ddeutil-workflow 0.0.7__py3-none-any.whl → 0.0.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.
@@ -29,7 +29,12 @@ from pydantic.functional_validators import model_validator
29
29
  from typing_extensions import Self
30
30
 
31
31
  from .__types import DictData, DictStr, Matrix, MatrixExclude, MatrixInclude
32
- from .exceptions import JobException, PipelineException, StageException
32
+ from .exceptions import (
33
+ JobException,
34
+ PipelineException,
35
+ StageException,
36
+ UtilException,
37
+ )
33
38
  from .loader import Loader
34
39
  from .on import On
35
40
  from .scheduler import CronRunner
@@ -39,6 +44,7 @@ from .utils import (
39
44
  Result,
40
45
  cross_product,
41
46
  dash2underscore,
47
+ filter_func,
42
48
  gen_id,
43
49
  get_diff_sec,
44
50
  )
@@ -54,7 +60,7 @@ class Strategy(BaseModel):
54
60
  ... 'fail-fast': False,
55
61
  ... 'matrix': {
56
62
  ... 'first': [1, 2, 3],
57
- ... 'second': ['foo', 'bar']
63
+ ... 'second': ['foo', 'bar'],
58
64
  ... },
59
65
  ... 'include': [{'first': 4, 'second': 'foo'}],
60
66
  ... 'exclude': [{'first': 1, 'second': 'bar'}],
@@ -82,6 +88,10 @@ class Strategy(BaseModel):
82
88
  dash2underscore("fail-fast", values)
83
89
  return values
84
90
 
91
+ def is_set(self) -> bool:
92
+ """Return True if this strategy was set from yaml template."""
93
+ return len(self.matrix) > 0
94
+
85
95
  def make(self) -> list[DictStr]:
86
96
  """Return List of product of matrix values that already filter with
87
97
  exclude and add include.
@@ -138,18 +148,25 @@ class Job(BaseModel):
138
148
  Data Validate:
139
149
  >>> job = {
140
150
  ... "runs-on": None,
141
- ... "strategy": {},
151
+ ... "strategy": {
152
+ ... "max-parallel": 1,
153
+ ... "matrix": {
154
+ ... "first": [1, 2, 3],
155
+ ... "second": ['foo', 'bar'],
156
+ ... },
157
+ ... },
142
158
  ... "needs": [],
143
159
  ... "stages": [
144
160
  ... {
145
161
  ... "name": "Some stage",
146
162
  ... "run": "print('Hello World')",
147
163
  ... },
164
+ ... ...
148
165
  ... ],
149
166
  ... }
150
167
  """
151
168
 
152
- name: Optional[str] = Field(default=None)
169
+ id: Optional[str] = Field(default=None)
153
170
  desc: Optional[str] = Field(default=None)
154
171
  runs_on: Optional[str] = Field(default=None)
155
172
  stages: list[Stage] = Field(
@@ -164,6 +181,9 @@ class Job(BaseModel):
164
181
  default_factory=Strategy,
165
182
  description="A strategy matrix that want to generate.",
166
183
  )
184
+ run_id: Optional[str] = Field(
185
+ default=None, description="A running job ID.", repr=False
186
+ )
167
187
 
168
188
  @model_validator(mode="before")
169
189
  def __prepare_keys(cls, values: DictData) -> DictData:
@@ -173,6 +193,12 @@ class Job(BaseModel):
173
193
  dash2underscore("runs-on", values)
174
194
  return values
175
195
 
196
+ @model_validator(mode="after")
197
+ def __prepare_running_id(self):
198
+ if self.run_id is None:
199
+ self.run_id = gen_id(self.id or "", unique=True)
200
+ return self
201
+
176
202
  def stage(self, stage_id: str) -> Stage:
177
203
  """Return stage model that match with an input stage ID."""
178
204
  for stage in self.stages:
@@ -180,9 +206,8 @@ class Job(BaseModel):
180
206
  return stage
181
207
  raise ValueError(f"Stage ID {stage_id} does not exists")
182
208
 
183
- @staticmethod
184
- def set_outputs(output: DictData) -> DictData:
185
- if len(output) > 1:
209
+ def set_outputs(self, output: DictData) -> DictData:
210
+ if len(output) > 1 and self.strategy.is_set():
186
211
  return {"strategies": output}
187
212
 
188
213
  return output[next(iter(output))]
@@ -194,26 +219,32 @@ class Job(BaseModel):
194
219
  *,
195
220
  event: Event | None = None,
196
221
  ) -> Result:
197
- """Strategy execution with passing dynamic parameters from the pipeline
198
- stage execution.
222
+ """Job Strategy execution with passing dynamic parameters from the
223
+ pipeline execution to strategy matrix.
199
224
 
200
- :param strategy:
201
- :param params:
225
+ This execution is the minimum level execution of job model.
226
+
227
+ :param strategy: A metrix strategy value.
228
+ :param params: A dynamic parameters.
202
229
  :param event: An manger event that pass to the PoolThreadExecutor.
203
230
  :rtype: Result
231
+
232
+ :raise JobException: If it has any error from StageException or
233
+ UtilException.
204
234
  """
205
- _stop_rs: Result = Result(
206
- status=1,
207
- context={
208
- gen_id(strategy): {
209
- "matrix": strategy,
210
- "stages": {},
211
- "error": "Event stopped",
212
- },
213
- },
214
- )
215
235
  if event and event.is_set():
216
- return _stop_rs
236
+ return Result(
237
+ status=1,
238
+ context={
239
+ gen_id(strategy): {
240
+ "matrix": strategy,
241
+ "stages": {},
242
+ "error": {
243
+ "message": "Process Event stopped before execution"
244
+ },
245
+ },
246
+ },
247
+ )
217
248
 
218
249
  # NOTE: Create strategy execution context and update a matrix and copied
219
250
  # of params. So, the context value will have structure like;
@@ -229,16 +260,25 @@ class Job(BaseModel):
229
260
 
230
261
  # IMPORTANT: The stage execution only run sequentially one-by-one.
231
262
  for stage in self.stages:
263
+
264
+ # IMPORTANT: Change any stage running IDs to this job running ID.
265
+ stage.run_id = self.run_id
266
+
232
267
  _st_name: str = stage.id or stage.name
233
268
 
234
- if stage.is_skip(params=context):
235
- logging.info(f"[JOB]: Skip the stage: {_st_name!r}")
269
+ if stage.is_skipped(params=context):
270
+ logging.info(
271
+ f"({self.run_id}) [JOB]: Skip the stage: {_st_name!r}"
272
+ )
236
273
  continue
237
- logging.info(f"[JOB]: Start execute the stage: {_st_name!r}")
274
+
275
+ logging.info(
276
+ f"({self.run_id}) [JOB]: Start execute the stage: {_st_name!r}"
277
+ )
238
278
 
239
279
  # NOTE: Logging a matrix that pass on this stage execution.
240
280
  if strategy:
241
- logging.info(f"[...]: Matrix: {strategy}")
281
+ logging.info(f"({self.run_id}) [JOB]: Matrix: {strategy}")
242
282
 
243
283
  # NOTE:
244
284
  # I do not use below syntax because `params` dict be the
@@ -258,23 +298,41 @@ class Job(BaseModel):
258
298
  # }
259
299
  #
260
300
  if event and event.is_set():
261
- return _stop_rs
262
- rs: Result = stage.execute(params=context)
263
- if rs.status == 0:
301
+ return Result(
302
+ status=1,
303
+ context={
304
+ gen_id(strategy): {
305
+ "matrix": strategy,
306
+ "stages": filter_func(context.pop("stages", {})),
307
+ "error": {
308
+ "message": (
309
+ "Process Event stopped before execution"
310
+ ),
311
+ },
312
+ },
313
+ },
314
+ )
315
+ try:
316
+ rs: Result = stage.execute(params=context)
264
317
  stage.set_outputs(rs.context, params=context)
265
- else:
266
- raise JobException(
267
- f"Getting status does not equal zero on stage: "
268
- f"{stage.name}."
318
+ except (StageException, UtilException) as err:
319
+ logging.error(
320
+ f"({self.run_id}) [JOB]: {err.__class__.__name__}: {err}"
269
321
  )
270
- # TODO: Filter and warning if it pass any objects to context between
271
- # strategy job executor like function, etc.
322
+ raise JobException(
323
+ f"Get stage execution error: {err.__class__.__name__}: "
324
+ f"{err}"
325
+ ) from None
272
326
  return Result(
273
327
  status=0,
274
328
  context={
275
329
  gen_id(strategy): {
276
330
  "matrix": strategy,
277
- "stages": context.pop("stages", {}),
331
+ # NOTE: (WF001) filter own created function from stages
332
+ # value, because it does not dump with pickle when you
333
+ # execute with multiprocess.
334
+ #
335
+ "stages": filter_func(context.pop("stages", {})),
278
336
  },
279
337
  },
280
338
  )
@@ -288,17 +346,20 @@ class Job(BaseModel):
288
346
  :rtype: Result
289
347
  """
290
348
  strategy_context: DictData = {}
291
- rs = Result(context=strategy_context)
292
349
 
293
- if self.strategy.max_parallel == 1:
350
+ # NOTE: Normal Job execution.
351
+ if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
294
352
  for strategy in self.strategy.make():
295
353
  rs: Result = self.strategy_execute(
296
354
  strategy, params=copy.deepcopy(params)
297
355
  )
298
356
  strategy_context.update(rs.context)
299
- return rs
357
+ return Result(
358
+ status=0,
359
+ context=strategy_context,
360
+ )
300
361
 
301
- # FIXME: (WF001) I got error that raise when use
362
+ # WARNING: (WF001) I got error that raise when use
302
363
  # ``ProcessPoolExecutor``;
303
364
  # ---
304
365
  # _pickle.PicklingError: Can't pickle
@@ -308,84 +369,109 @@ class Job(BaseModel):
308
369
  with Manager() as manager:
309
370
  event: Event = manager.Event()
310
371
 
372
+ # NOTE: Start process pool executor for running strategy executor in
373
+ # parallel mode.
311
374
  with ProcessPoolExecutor(
312
375
  max_workers=self.strategy.max_parallel
313
- ) as pool:
314
- pool_result: list[Future] = [
315
- pool.submit(
376
+ ) as executor:
377
+ features: list[Future] = [
378
+ executor.submit(
316
379
  self.strategy_execute,
317
- st,
380
+ strategy,
318
381
  params=copy.deepcopy(params),
319
382
  event=event,
320
383
  )
321
- for st in self.strategy.make()
384
+ for strategy in self.strategy.make()
322
385
  ]
323
386
  if self.strategy.fail_fast:
387
+ rs = self.__catch_fail_fast(event, features)
388
+ else:
389
+ rs = self.__catch_all_completed(features)
390
+ return Result(
391
+ status=0,
392
+ context=rs.context,
393
+ )
324
394
 
325
- # NOTE: Get results from a collection of tasks with a
326
- # timeout that has the first exception.
327
- done, not_done = wait(
328
- pool_result, timeout=60, return_when=FIRST_EXCEPTION
329
- )
330
- nd: str = (
331
- f", the strategies do not run is {not_done}"
332
- if not_done
333
- else ""
334
- )
335
- logging.warning(f"[JOB]: Strategy is set Fail Fast{nd}")
336
-
337
- # NOTE: Stop all running tasks
338
- event.set()
339
-
340
- # NOTE: Cancel any scheduled tasks
341
- for future in pool_result:
342
- future.cancel()
343
-
344
- rs.status = 0
345
- for f in done:
346
- if f.exception():
347
- rs.status = 1
348
- logging.error(
349
- f"One task failed with: {f.exception()}, "
350
- f"shutting down"
351
- )
352
- elif f.cancelled():
353
- continue
354
- else:
355
- rs: Result = f.result(timeout=60)
356
- strategy_context.update(rs.context)
357
- rs.context = strategy_context
358
- return rs
359
-
360
- for pool_rs in as_completed(pool_result):
361
- try:
362
- rs: Result = pool_rs.result(timeout=60)
363
- strategy_context.update(rs.context)
364
- except PickleError as err:
365
- # NOTE: I do not want to fix this issue because it does
366
- # not make sense and over-engineering with this bug
367
- # fix process.
368
- raise JobException(
369
- f"PyStage that create object on locals does use "
370
- f"parallel in strategy;\n\t{err}"
371
- ) from None
372
- except TimeoutError:
373
- rs.status = 1
374
- logging.warning("Task is hanging. Attempting to kill.")
375
- pool_rs.cancel()
376
- if not pool_rs.cancelled():
377
- logging.warning("Failed to cancel the task.")
378
- else:
379
- logging.warning("Task canceled successfully.")
380
- except StageException as err:
381
- rs.status = 1
382
- logging.warning(
383
- f"Get stage exception with fail-fast does not set;"
384
- f"\n\t{err}"
385
- )
386
- rs.status = 0
387
- rs.context = strategy_context
388
- return rs
395
+ def __catch_fail_fast(self, event: Event, features: list[Future]) -> Result:
396
+ """Job parallel pool features catching with fail-fast mode. That will
397
+ stop all not done features if it receive the first exception from all
398
+ running features.
399
+
400
+ :param event:
401
+ :param features: A list of features.
402
+ :rtype: Result
403
+ """
404
+ strategy_context: DictData = {}
405
+ # NOTE: Get results from a collection of tasks with a
406
+ # timeout that has the first exception.
407
+ done, not_done = wait(
408
+ features, timeout=1800, return_when=FIRST_EXCEPTION
409
+ )
410
+ nd: str = (
411
+ f", the strategies do not run is {not_done}" if not_done else ""
412
+ )
413
+ logging.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
414
+
415
+ # NOTE: Stop all running tasks
416
+ event.set()
417
+
418
+ # NOTE: Cancel any scheduled tasks
419
+ for future in features:
420
+ future.cancel()
421
+
422
+ status: int = 0
423
+ for f in done:
424
+ if f.exception():
425
+ status = 1
426
+ logging.error(
427
+ f"({self.run_id}) [JOB]: One stage failed with: "
428
+ f"{f.exception()}, shutting down this feature."
429
+ )
430
+ elif f.cancelled():
431
+ continue
432
+ else:
433
+ rs: Result = f.result(timeout=60)
434
+ strategy_context.update(rs.context)
435
+ return Result(
436
+ status=status,
437
+ context=strategy_context,
438
+ )
439
+
440
+ def __catch_all_completed(self, features: list[Future]) -> Result:
441
+ """Job parallel pool features catching with all-completed mode.
442
+
443
+ :param features: A list of features.
444
+ """
445
+ strategy_context: DictData = {}
446
+ status: int = 0
447
+ for feature in as_completed(features):
448
+ try:
449
+ rs: Result = feature.result(timeout=60)
450
+ strategy_context.update(rs.context)
451
+ except PickleError as err:
452
+ # NOTE: (WF001) I do not want to fix this issue because
453
+ # it does not make sense and over-engineering with
454
+ # this bug fix process.
455
+ raise JobException(
456
+ f"PyStage that create object on locals does use "
457
+ f"parallel in strategy execution;\n\t{err}"
458
+ ) from None
459
+ except TimeoutError:
460
+ status = 1
461
+ logging.warning("Task is hanging. Attempting to kill.")
462
+ feature.cancel()
463
+ if not feature.cancelled():
464
+ logging.warning("Failed to cancel the task.")
465
+ else:
466
+ logging.warning("Task canceled successfully.")
467
+ except JobException as err:
468
+ status = 1
469
+ logging.error(
470
+ f"({self.run_id}) [JOB]: Get stage exception with "
471
+ f"fail-fast does not set;\n{err.__class__.__name__}:\n\t"
472
+ f"{err}"
473
+ )
474
+ return Result(status=status, context=strategy_context)
389
475
 
390
476
 
391
477
  class Pipeline(BaseModel):
@@ -414,6 +500,9 @@ class Pipeline(BaseModel):
414
500
  default_factory=dict,
415
501
  description="A mapping of job ID and job model that already loaded.",
416
502
  )
503
+ run_id: Optional[str] = Field(
504
+ default=None, description="A running job ID.", repr=False
505
+ )
417
506
 
418
507
  @classmethod
419
508
  def from_loader(
@@ -474,7 +563,7 @@ class Pipeline(BaseModel):
474
563
  return values
475
564
 
476
565
  @model_validator(mode="after")
477
- def __validate_jobs_need(self):
566
+ def __validate_jobs_need_and_prepare_running_id(self):
478
567
  for job in self.jobs:
479
568
  if not_exist := [
480
569
  need for need in self.jobs[job].needs if need not in self.jobs
@@ -483,6 +572,13 @@ class Pipeline(BaseModel):
483
572
  f"This needed jobs: {not_exist} do not exist in this "
484
573
  f"pipeline."
485
574
  )
575
+
576
+ # NOTE: update a job id with its job id from pipeline template
577
+ self.jobs[job].id = job
578
+
579
+ if self.run_id is None:
580
+ self.run_id = gen_id(self.name, unique=True)
581
+
486
582
  return self
487
583
 
488
584
  def job(self, name: str) -> Job:
@@ -602,7 +698,7 @@ class Pipeline(BaseModel):
602
698
  self,
603
699
  job: str,
604
700
  params: DictData,
605
- ):
701
+ ) -> Result:
606
702
  """Job Executor that use on pipeline executor.
607
703
  :param job: A job ID that want to execute.
608
704
  :param params: A params that was parameterized from pipeline execution.
@@ -613,18 +709,19 @@ class Pipeline(BaseModel):
613
709
  f"The job ID: {job} does not exists on {self.name!r} pipeline."
614
710
  )
615
711
 
616
- job_obj: Job = self.jobs[job]
617
-
618
- rs: Result = job_obj.execute(params=params)
619
- if rs.status != 0:
620
- logging.warning(
621
- f"Getting status does not equal zero on job: {job}."
622
- )
623
- return Result(
624
- status=1, context={job: job_obj.set_outputs(rs.context)}
625
- )
626
-
627
- return Result(status=0, context={job: job_obj.set_outputs(rs.context)})
712
+ try:
713
+ logging.info(f"({self.run_id}) [PIPELINE]: Start execute: {job!r}")
714
+ job_obj: Job = self.jobs[job]
715
+ j_rs: Result = job_obj.execute(params=params)
716
+ except JobException as err:
717
+ raise PipelineException(
718
+ f"The job ID: {job} get raise error: {err.__class__.__name__}:"
719
+ f"\n{err}"
720
+ ) from None
721
+ return Result(
722
+ status=j_rs.status,
723
+ context={job: job_obj.set_outputs(j_rs.context)},
724
+ )
628
725
 
629
726
  def execute(
630
727
  self,
@@ -666,68 +763,131 @@ class Pipeline(BaseModel):
666
763
  logging.warning("[PIPELINE]: This pipeline does not have any jobs")
667
764
  return Result(status=0, context=params)
668
765
 
669
- # NOTE: create a job queue that keep the job that want to running after
766
+ # NOTE: Create a job queue that keep the job that want to running after
670
767
  # it dependency condition.
671
768
  jq: Queue = Queue()
672
769
  for job_id in self.jobs:
673
770
  jq.put(job_id)
674
771
 
772
+ # NOTE: Create start timestamp
675
773
  ts: float = time.monotonic()
676
- not_time_out_flag: bool = True
677
774
 
678
775
  # NOTE: Create result context that will pass this context to any
679
776
  # execution dependency.
680
777
  rs: Result = Result(context=self.parameterize(params))
681
- if (
682
- worker := int(os.getenv("WORKFLOW_CORE_MAX_JOB_PARALLEL", "1"))
683
- ) > 1:
684
- # IMPORTANT: The job execution can run parallel and waiting by
685
- # needed.
686
- with ThreadPoolExecutor(max_workers=worker) as executor:
687
- futures: list[Future] = []
688
- while not jq.empty() and (
689
- not_time_out_flag := ((time.monotonic() - ts) < timeout)
690
- ):
691
- job_id: str = jq.get()
692
- logging.info(
693
- f"[PIPELINE]: Start execute the job: {job_id!r}"
694
- )
695
- job: Job = self.jobs[job_id]
696
- if any(
697
- need not in rs.context["jobs"] for need in job.needs
698
- ):
699
- jq.put(job_id)
700
- futures.append(
701
- executor.submit(
702
- self.job_execute,
703
- job_id,
704
- params=copy.deepcopy(rs.context),
705
- ),
778
+ try:
779
+ rs.receive(
780
+ self.__exec_non_threading(rs, jq, ts, timeout=timeout)
781
+ if (
782
+ worker := int(
783
+ os.getenv("WORKFLOW_CORE_MAX_JOB_PARALLEL", "1")
706
784
  )
707
- for future in as_completed(futures):
708
- job_rs: Result = future.result(timeout=20)
709
- rs.context["jobs"].update(job_rs.context)
710
- else:
711
- logging.info(
712
- f"[CORE]: Run {self.name} with non-threading job executor"
785
+ )
786
+ == 1
787
+ else self.__exec_threading(
788
+ rs, jq, ts, worker=worker, timeout=timeout
789
+ )
713
790
  )
714
- while not jq.empty() and (
791
+ return rs
792
+ except PipelineException as err:
793
+ rs.context.update({"error": {"message": str(err)}})
794
+ rs.status = 1
795
+ return rs
796
+
797
+ def __exec_threading(
798
+ self,
799
+ rs: Result,
800
+ job_queue: Queue,
801
+ ts: float,
802
+ *,
803
+ worker: int = 1,
804
+ timeout: int = 600,
805
+ ) -> Result:
806
+ """Pipeline threading execution."""
807
+ not_time_out_flag: bool = True
808
+
809
+ # IMPORTANT: The job execution can run parallel and waiting by
810
+ # needed.
811
+ with ThreadPoolExecutor(max_workers=worker) as executor:
812
+ futures: list[Future] = []
813
+ while not job_queue.empty() and (
715
814
  not_time_out_flag := ((time.monotonic() - ts) < timeout)
716
815
  ):
717
- job_id: str = jq.get()
718
- logging.info(f"[PIPELINE]: Start execute the job: {job_id!r}")
816
+ job_id: str = job_queue.get()
719
817
  job: Job = self.jobs[job_id]
720
- if any(need not in rs.context["jobs"] for need in job.needs):
721
- jq.put(job_id)
722
818
 
723
- job_rs = self.job_execute(
724
- job_id, params=copy.deepcopy(rs.context)
819
+ # IMPORTANT:
820
+ # Change any job running IDs to this pipeline running ID.
821
+ job.run_id = self.run_id
822
+
823
+ if any(need not in rs.context["jobs"] for need in job.needs):
824
+ job_queue.put(job_id)
825
+ time.sleep(0.5)
826
+ continue
827
+
828
+ futures.append(
829
+ executor.submit(
830
+ self.job_execute,
831
+ job_id,
832
+ params=copy.deepcopy(rs.context),
833
+ ),
725
834
  )
726
- rs.context["jobs"].update(job_rs.context)
835
+
836
+ for future in as_completed(futures):
837
+ if err := future.exception():
838
+ logging.error(f"{err}")
839
+ raise PipelineException(f"{err}")
840
+
841
+ # NOTE: Update job result to pipeline result.
842
+ rs.receive_jobs(future.result(timeout=20))
727
843
 
728
844
  if not not_time_out_flag:
729
- logging.warning("Execution of pipeline was time out")
730
- rs.status = 1
731
- return rs
845
+ logging.warning(
846
+ f"({self.run_id}) [PIPELINE]: Execution of pipeline was timeout"
847
+ )
848
+ raise PipelineException(
849
+ f"Execution of pipeline: {self.name} was timeout"
850
+ )
851
+ rs.status = 0
852
+ return rs
853
+
854
+ def __exec_non_threading(
855
+ self,
856
+ rs: Result,
857
+ job_queue: Queue,
858
+ ts: float,
859
+ *,
860
+ timeout: int = 600,
861
+ ) -> Result:
862
+ """Pipeline non-threading execution."""
863
+ not_time_out_flag: bool = True
864
+ logging.info(f"[CORE]: Run {self.name} with non-threading job executor")
865
+ while not job_queue.empty() and (
866
+ not_time_out_flag := ((time.monotonic() - ts) < timeout)
867
+ ):
868
+ job_id: str = job_queue.get()
869
+ job: Job = self.jobs[job_id]
870
+
871
+ # IMPORTANT:
872
+ # Change any job running IDs to this pipeline running ID.
873
+ job.run_id = self.run_id
874
+
875
+ # NOTE:
876
+ if any(need not in rs.context["jobs"] for need in job.needs):
877
+ job_queue.put(job_id)
878
+ time.sleep(0.5)
879
+ continue
880
+
881
+ # NOTE: Start job execution.
882
+ job_rs = self.job_execute(job_id, params=copy.deepcopy(rs.context))
883
+ rs.context["jobs"].update(job_rs.context)
884
+
885
+ if not not_time_out_flag:
886
+ logging.warning(
887
+ f"({self.run_id}) [PIPELINE]: Execution of pipeline was timeout"
888
+ )
889
+ raise PipelineException(
890
+ f"Execution of pipeline: {self.name} was timeout"
891
+ )
732
892
  rs.status = 0
733
893
  return rs