ddeutil-workflow 0.0.36__py3-none-any.whl → 0.0.38__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +4 -1
- ddeutil/workflow/api/api.py +3 -1
- ddeutil/workflow/api/log.py +59 -0
- ddeutil/workflow/api/repeat.py +1 -1
- ddeutil/workflow/api/routes/job.py +4 -2
- ddeutil/workflow/api/routes/logs.py +126 -17
- ddeutil/workflow/api/routes/schedules.py +6 -6
- ddeutil/workflow/api/routes/workflows.py +9 -7
- ddeutil/workflow/caller.py +9 -3
- ddeutil/workflow/conf.py +0 -60
- ddeutil/workflow/context.py +59 -0
- ddeutil/workflow/exceptions.py +14 -1
- ddeutil/workflow/job.py +310 -277
- ddeutil/workflow/logs.py +6 -1
- ddeutil/workflow/result.py +1 -1
- ddeutil/workflow/scheduler.py +11 -4
- ddeutil/workflow/stages.py +368 -111
- ddeutil/workflow/utils.py +27 -49
- ddeutil/workflow/workflow.py +137 -72
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/METADATA +12 -6
- ddeutil_workflow-0.0.38.dist-info/RECORD +33 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.36.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info/licenses}/LICENSE +0 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/top_level.txt +0 -0
    
        ddeutil/workflow/job.py
    CHANGED
    
    | @@ -27,7 +27,7 @@ from threading import Event | |
| 27 27 | 
             
            from typing import Annotated, Any, Literal, Optional, Union
         | 
| 28 28 |  | 
| 29 29 | 
             
            from ddeutil.core import freeze_args
         | 
| 30 | 
            -
            from pydantic import BaseModel, ConfigDict, Field
         | 
| 30 | 
            +
            from pydantic import BaseModel, ConfigDict, Discriminator, Field, Tag
         | 
| 31 31 | 
             
            from pydantic.functional_validators import field_validator, model_validator
         | 
| 32 32 | 
             
            from typing_extensions import Self
         | 
| 33 33 |  | 
| @@ -43,7 +43,6 @@ from .stages import Stage | |
| 43 43 | 
             
            from .templates import has_template
         | 
| 44 44 | 
             
            from .utils import (
         | 
| 45 45 | 
             
                cross_product,
         | 
| 46 | 
            -
                dash2underscore,
         | 
| 47 46 | 
             
                filter_func,
         | 
| 48 47 | 
             
                gen_id,
         | 
| 49 48 | 
             
            )
         | 
| @@ -60,6 +59,8 @@ __all__: TupleStr = ( | |
| 60 59 | 
             
                "RunsOnSelfHosted",
         | 
| 61 60 | 
             
                "RunsOnK8s",
         | 
| 62 61 | 
             
                "make",
         | 
| 62 | 
            +
                "local_execute_strategy",
         | 
| 63 | 
            +
                "local_execute",
         | 
| 63 64 | 
             
            )
         | 
| 64 65 |  | 
| 65 66 |  | 
| @@ -153,7 +154,7 @@ class Strategy(BaseModel): | |
| 153 154 |  | 
| 154 155 | 
             
                fail_fast: bool = Field(
         | 
| 155 156 | 
             
                    default=False,
         | 
| 156 | 
            -
                     | 
| 157 | 
            +
                    alias="fail-fast",
         | 
| 157 158 | 
             
                )
         | 
| 158 159 | 
             
                max_parallel: int = Field(
         | 
| 159 160 | 
             
                    default=1,
         | 
| @@ -162,7 +163,7 @@ class Strategy(BaseModel): | |
| 162 163 | 
             
                        "The maximum number of executor thread pool that want to run "
         | 
| 163 164 | 
             
                        "parallel"
         | 
| 164 165 | 
             
                    ),
         | 
| 165 | 
            -
                     | 
| 166 | 
            +
                    alias="max-parallel",
         | 
| 166 167 | 
             
                )
         | 
| 167 168 | 
             
                matrix: Matrix = Field(
         | 
| 168 169 | 
             
                    default_factory=dict,
         | 
| @@ -179,18 +180,6 @@ class Strategy(BaseModel): | |
| 179 180 | 
             
                    description="A list of exclude matrix that want to filter-out.",
         | 
| 180 181 | 
             
                )
         | 
| 181 182 |  | 
| 182 | 
            -
                @model_validator(mode="before")
         | 
| 183 | 
            -
                def __prepare_keys(cls, values: DictData) -> DictData:
         | 
| 184 | 
            -
                    """Rename key that use dash to underscore because Python does not
         | 
| 185 | 
            -
                    support this character exist in any variable name.
         | 
| 186 | 
            -
             | 
| 187 | 
            -
                    :param values: A parsing values to these models
         | 
| 188 | 
            -
                    :rtype: DictData
         | 
| 189 | 
            -
                    """
         | 
| 190 | 
            -
                    dash2underscore("max-parallel", values)
         | 
| 191 | 
            -
                    dash2underscore("fail-fast", values)
         | 
| 192 | 
            -
                    return values
         | 
| 193 | 
            -
             | 
| 194 183 | 
             
                def is_set(self) -> bool:
         | 
| 195 184 | 
             
                    """Return True if this strategy was set from yaml template.
         | 
| 196 185 |  | 
| @@ -228,6 +217,10 @@ class RunsOnType(str, Enum): | |
| 228 217 |  | 
| 229 218 |  | 
| 230 219 | 
             
            class BaseRunsOn(BaseModel):  # pragma: no cov
         | 
| 220 | 
            +
                """Base Runs-On Model for generate runs-on types via inherit this model
         | 
| 221 | 
            +
                object and override execute method.
         | 
| 222 | 
            +
                """
         | 
| 223 | 
            +
             | 
| 231 224 | 
             
                model_config = ConfigDict(use_enum_values=True)
         | 
| 232 225 |  | 
| 233 226 | 
             
                type: Literal[RunsOnType.LOCAL]
         | 
| @@ -243,12 +236,17 @@ class RunsOnLocal(BaseRunsOn):  # pragma: no cov | |
| 243 236 | 
             
                type: Literal[RunsOnType.LOCAL] = Field(default=RunsOnType.LOCAL)
         | 
| 244 237 |  | 
| 245 238 |  | 
| 239 | 
            +
            class SelfHostedArgs(BaseModel):
         | 
| 240 | 
            +
                host: str
         | 
| 241 | 
            +
             | 
| 242 | 
            +
             | 
| 246 243 | 
             
            class RunsOnSelfHosted(BaseRunsOn):  # pragma: no cov
         | 
| 247 244 | 
             
                """Runs-on self-hosted."""
         | 
| 248 245 |  | 
| 249 246 | 
             
                type: Literal[RunsOnType.SELF_HOSTED] = Field(
         | 
| 250 247 | 
             
                    default=RunsOnType.SELF_HOSTED
         | 
| 251 248 | 
             
                )
         | 
| 249 | 
            +
                args: SelfHostedArgs = Field(alias="with")
         | 
| 252 250 |  | 
| 253 251 |  | 
| 254 252 | 
             
            class RunsOnK8s(BaseRunsOn):  # pragma: no cov
         | 
| @@ -257,13 +255,17 @@ class RunsOnK8s(BaseRunsOn):  # pragma: no cov | |
| 257 255 | 
             
                type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
         | 
| 258 256 |  | 
| 259 257 |  | 
| 258 | 
            +
            def get_discriminator_runs_on(model: dict[str, Any]) -> str:
         | 
| 259 | 
            +
                return model.get("type", "local")
         | 
| 260 | 
            +
             | 
| 261 | 
            +
             | 
| 260 262 | 
             
            RunsOn = Annotated[
         | 
| 261 263 | 
             
                Union[
         | 
| 262 | 
            -
                     | 
| 263 | 
            -
                    RunsOnSelfHosted,
         | 
| 264 | 
            -
                     | 
| 264 | 
            +
                    Annotated[RunsOnK8s, Tag(RunsOnType.K8S)],
         | 
| 265 | 
            +
                    Annotated[RunsOnSelfHosted, Tag(RunsOnType.SELF_HOSTED)],
         | 
| 266 | 
            +
                    Annotated[RunsOnLocal, Tag(RunsOnType.LOCAL)],
         | 
| 265 267 | 
             
                ],
         | 
| 266 | 
            -
                 | 
| 268 | 
            +
                Discriminator(get_discriminator_runs_on),
         | 
| 267 269 | 
             
            ]
         | 
| 268 270 |  | 
| 269 271 |  | 
| @@ -308,7 +310,7 @@ class Job(BaseModel): | |
| 308 310 | 
             
                runs_on: RunsOn = Field(
         | 
| 309 311 | 
             
                    default_factory=RunsOnLocal,
         | 
| 310 312 | 
             
                    description="A target node for this job to use for execution.",
         | 
| 311 | 
            -
                     | 
| 313 | 
            +
                    alias="runs-on",
         | 
| 312 314 | 
             
                )
         | 
| 313 315 | 
             
                stages: list[Stage] = Field(
         | 
| 314 316 | 
             
                    default_factory=list,
         | 
| @@ -317,7 +319,7 @@ class Job(BaseModel): | |
| 317 319 | 
             
                trigger_rule: TriggerRules = Field(
         | 
| 318 320 | 
             
                    default=TriggerRules.all_success,
         | 
| 319 321 | 
             
                    description="A trigger rule of tracking needed jobs.",
         | 
| 320 | 
            -
                     | 
| 322 | 
            +
                    alias="trigger-rule",
         | 
| 321 323 | 
             
                )
         | 
| 322 324 | 
             
                needs: list[str] = Field(
         | 
| 323 325 | 
             
                    default_factory=list,
         | 
| @@ -328,18 +330,6 @@ class Job(BaseModel): | |
| 328 330 | 
             
                    description="A strategy matrix that want to generate.",
         | 
| 329 331 | 
             
                )
         | 
| 330 332 |  | 
| 331 | 
            -
                @model_validator(mode="before")
         | 
| 332 | 
            -
                def __prepare_keys__(cls, values: DictData) -> DictData:
         | 
| 333 | 
            -
                    """Rename key that use dash to underscore because Python does not
         | 
| 334 | 
            -
                    support this character exist in any variable name.
         | 
| 335 | 
            -
             | 
| 336 | 
            -
                    :param values: A passing value that coming for initialize this object.
         | 
| 337 | 
            -
                    :rtype: DictData
         | 
| 338 | 
            -
                    """
         | 
| 339 | 
            -
                    dash2underscore("runs-on", values)
         | 
| 340 | 
            -
                    dash2underscore("trigger-rule", values)
         | 
| 341 | 
            -
                    return values
         | 
| 342 | 
            -
             | 
| 343 333 | 
             
                @field_validator("desc", mode="after")
         | 
| 344 334 | 
             
                def ___prepare_desc__(cls, value: str) -> str:
         | 
| 345 335 | 
             
                    """Prepare description string that was created on a template.
         | 
| @@ -430,15 +420,14 @@ class Job(BaseModel): | |
| 430 420 |  | 
| 431 421 | 
             
                    :rtype: DictData
         | 
| 432 422 | 
             
                    """
         | 
| 423 | 
            +
                    if "jobs" not in to:
         | 
| 424 | 
            +
                        to["jobs"] = {}
         | 
| 425 | 
            +
             | 
| 433 426 | 
             
                    if self.id is None and not config.job_default_id:
         | 
| 434 427 | 
             
                        raise JobException(
         | 
| 435 428 | 
             
                            "This job do not set the ID before setting execution output."
         | 
| 436 429 | 
             
                        )
         | 
| 437 430 |  | 
| 438 | 
            -
                    # NOTE: Create jobs key to receive an output from the job execution.
         | 
| 439 | 
            -
                    if "jobs" not in to:
         | 
| 440 | 
            -
                        to["jobs"] = {}
         | 
| 441 | 
            -
             | 
| 442 431 | 
             
                    # NOTE: If the job ID did not set, it will use index of jobs key
         | 
| 443 432 | 
             
                    #   instead.
         | 
| 444 433 | 
             
                    _id: str = self.id or str(len(to["jobs"]) + 1)
         | 
| @@ -447,278 +436,322 @@ class Job(BaseModel): | |
| 447 436 | 
             
                        {"errors": output.pop("errors", {})} if "errors" in output else {}
         | 
| 448 437 | 
             
                    )
         | 
| 449 438 |  | 
| 450 | 
            -
                     | 
| 451 | 
            -
                        {"strategies": output, **errors}
         | 
| 452 | 
            -
             | 
| 453 | 
            -
                         | 
| 454 | 
            -
             | 
| 439 | 
            +
                    if self.strategy.is_set():
         | 
| 440 | 
            +
                        to["jobs"][_id] = {"strategies": output, **errors}
         | 
| 441 | 
            +
                    else:
         | 
| 442 | 
            +
                        _output = output.get(next(iter(output), "FIRST"), {})
         | 
| 443 | 
            +
                        _output.pop("matrix", {})
         | 
| 444 | 
            +
                        to["jobs"][_id] = {**_output, **errors}
         | 
| 455 445 | 
             
                    return to
         | 
| 456 446 |  | 
| 457 | 
            -
                def  | 
| 447 | 
            +
                def execute(
         | 
| 458 448 | 
             
                    self,
         | 
| 459 | 
            -
                    strategy: DictData,
         | 
| 460 449 | 
             
                    params: DictData,
         | 
| 461 450 | 
             
                    *,
         | 
| 451 | 
            +
                    run_id: str | None = None,
         | 
| 452 | 
            +
                    parent_run_id: str | None = None,
         | 
| 462 453 | 
             
                    result: Result | None = None,
         | 
| 463 454 | 
             
                    event: Event | None = None,
         | 
| 464 455 | 
             
                ) -> Result:
         | 
| 465 | 
            -
                    """Job  | 
| 466 | 
            -
                     | 
| 467 | 
            -
             | 
| 468 | 
            -
                        This execution is the minimum level of execution of this job model.
         | 
| 469 | 
            -
                    It different with `self.execute` because this method run only one
         | 
| 470 | 
            -
                    strategy and return with context of this strategy data.
         | 
| 471 | 
            -
             | 
| 472 | 
            -
                        The result of this execution will return result with strategy ID
         | 
| 473 | 
            -
                    that generated from the `gen_id` function with an input strategy value.
         | 
| 474 | 
            -
             | 
| 475 | 
            -
                    :raise JobException: If it has any error from `StageException` or
         | 
| 476 | 
            -
                        `UtilException`.
         | 
| 456 | 
            +
                    """Job execution with passing dynamic parameters from the workflow
         | 
| 457 | 
            +
                    execution. It will generate matrix values at the first step and run
         | 
| 458 | 
            +
                    multithread on this metrics to the `stages` field of this job.
         | 
| 477 459 |  | 
| 478 | 
            -
                    :param  | 
| 479 | 
            -
             | 
| 480 | 
            -
                    :param  | 
| 460 | 
            +
                    :param params: An input parameters that use on job execution.
         | 
| 461 | 
            +
                    :param run_id: A job running ID for this execution.
         | 
| 462 | 
            +
                    :param parent_run_id: A parent workflow running ID for this release.
         | 
| 481 463 | 
             
                    :param result: (Result) A result object for keeping context and status
         | 
| 482 464 | 
             
                        data.
         | 
| 483 | 
            -
                    :param event: An event manager that pass to the | 
| 465 | 
            +
                    :param event: (Event) An event manager that pass to the
         | 
| 466 | 
            +
                        PoolThreadExecutor.
         | 
| 484 467 |  | 
| 485 468 | 
             
                    :rtype: Result
         | 
| 486 469 | 
             
                    """
         | 
| 487 | 
            -
                     | 
| 488 | 
            -
                        result | 
| 470 | 
            +
                    result: Result = Result.construct_with_rs_or_id(
         | 
| 471 | 
            +
                        result,
         | 
| 472 | 
            +
                        run_id=run_id,
         | 
| 473 | 
            +
                        parent_run_id=parent_run_id,
         | 
| 474 | 
            +
                        id_logic=(self.id or "not-set"),
         | 
| 475 | 
            +
                    )
         | 
| 476 | 
            +
             | 
| 477 | 
            +
                    if self.runs_on.type == RunsOnType.LOCAL:
         | 
| 478 | 
            +
                        return local_execute(
         | 
| 479 | 
            +
                            job=self,
         | 
| 480 | 
            +
                            params=params,
         | 
| 481 | 
            +
                            result=result,
         | 
| 482 | 
            +
                            event=event,
         | 
| 483 | 
            +
                        )
         | 
| 484 | 
            +
                    elif self.runs_on.type == RunsOnType.SELF_HOSTED:  # pragma: no cov
         | 
| 485 | 
            +
                        pass
         | 
| 486 | 
            +
                    elif self.runs_on.type == RunsOnType.K8S:  # pragma: no cov
         | 
| 487 | 
            +
                        pass
         | 
| 488 | 
            +
             | 
| 489 | 
            +
                    # pragma: no cov
         | 
| 490 | 
            +
                    result.trace.error(
         | 
| 491 | 
            +
                        f"[JOB]: Job executor does not support for runs-on type: "
         | 
| 492 | 
            +
                        f"{self.runs_on.type} yet"
         | 
| 493 | 
            +
                    )
         | 
| 494 | 
            +
                    raise NotImplementedError(
         | 
| 495 | 
            +
                        f"The job runs-on other type: {self.runs_on.type} does not "
         | 
| 496 | 
            +
                        f"support yet."
         | 
| 497 | 
            +
                    )
         | 
| 498 | 
            +
             | 
| 499 | 
            +
             | 
| 500 | 
            +
            def local_execute_strategy(
         | 
| 501 | 
            +
                job: Job,
         | 
| 502 | 
            +
                strategy: DictData,
         | 
| 503 | 
            +
                params: DictData,
         | 
| 504 | 
            +
                *,
         | 
| 505 | 
            +
                result: Result | None = None,
         | 
| 506 | 
            +
                event: Event | None = None,
         | 
| 507 | 
            +
                raise_error: bool = False,
         | 
| 508 | 
            +
            ) -> Result:
         | 
| 509 | 
            +
                """Local job strategy execution with passing dynamic parameters from the
         | 
| 510 | 
            +
                workflow execution to strategy matrix.
         | 
| 511 | 
            +
             | 
| 512 | 
            +
                    This execution is the minimum level of execution of this job model.
         | 
| 513 | 
            +
                It different with `self.execute` because this method run only one
         | 
| 514 | 
            +
                strategy and return with context of this strategy data.
         | 
| 515 | 
            +
             | 
| 516 | 
            +
                    The result of this execution will return result with strategy ID
         | 
| 517 | 
            +
                that generated from the `gen_id` function with an input strategy value.
         | 
| 518 | 
            +
             | 
| 519 | 
            +
                :raise JobException: If it has any error from `StageException` or
         | 
| 520 | 
            +
                    `UtilException`.
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                :param job: (Job) A job model that want to execute.
         | 
| 523 | 
            +
                :param strategy: A strategy metrix value that use on this execution.
         | 
| 524 | 
            +
                    This value will pass to the `matrix` key for templating.
         | 
| 525 | 
            +
                :param params: A dynamic parameters that will deepcopy to the context.
         | 
| 526 | 
            +
                :param result: (Result) A result object for keeping context and status
         | 
| 527 | 
            +
                    data.
         | 
| 528 | 
            +
                :param event: (Event) An event manager that pass to the PoolThreadExecutor.
         | 
| 529 | 
            +
                :param raise_error: (bool) A flag that all this method raise error
         | 
| 530 | 
            +
             | 
| 531 | 
            +
                :rtype: Result
         | 
| 532 | 
            +
                """
         | 
| 533 | 
            +
                if result is None:
         | 
| 534 | 
            +
                    result: Result = Result(run_id=gen_id(job.id or "not-set", unique=True))
         | 
| 535 | 
            +
             | 
| 536 | 
            +
                strategy_id: str = gen_id(strategy)
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                # PARAGRAPH:
         | 
| 539 | 
            +
                #
         | 
| 540 | 
            +
                #       Create strategy execution context and update a matrix and copied
         | 
| 541 | 
            +
                #   of params. So, the context value will have structure like;
         | 
| 542 | 
            +
                #
         | 
| 543 | 
            +
                #   {
         | 
| 544 | 
            +
                #       "params": { ... },      <== Current input params
         | 
| 545 | 
            +
                #       "jobs": { ... },        <== Current input params
         | 
| 546 | 
            +
                #       "matrix": { ... }       <== Current strategy value
         | 
| 547 | 
            +
                #       "stages": { ... }       <== Catching stage outputs
         | 
| 548 | 
            +
                #   }
         | 
| 549 | 
            +
                #
         | 
| 550 | 
            +
                context: DictData = copy.deepcopy(params)
         | 
| 551 | 
            +
                context.update({"matrix": strategy, "stages": {}})
         | 
| 552 | 
            +
             | 
| 553 | 
            +
                if strategy:
         | 
| 554 | 
            +
                    result.trace.info(f"[JOB]: Execute Strategy ID: {strategy_id}")
         | 
| 555 | 
            +
                    result.trace.info(f"[JOB]: ... Matrix: {strategy_id}")
         | 
| 556 | 
            +
             | 
| 557 | 
            +
                # IMPORTANT: The stage execution only run sequentially one-by-one.
         | 
| 558 | 
            +
                for stage in job.stages:
         | 
| 559 | 
            +
             | 
| 560 | 
            +
                    if stage.is_skipped(params=context):
         | 
| 561 | 
            +
                        result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
         | 
| 562 | 
            +
                        continue
         | 
| 489 563 |  | 
| 490 | 
            -
                     | 
| 564 | 
            +
                    if event and event.is_set():
         | 
| 565 | 
            +
                        error_msg: str = (
         | 
| 566 | 
            +
                            "Job strategy was canceled from event that had set before "
         | 
| 567 | 
            +
                            "strategy execution."
         | 
| 568 | 
            +
                        )
         | 
| 569 | 
            +
                        return result.catch(
         | 
| 570 | 
            +
                            status=Status.FAILED,
         | 
| 571 | 
            +
                            context={
         | 
| 572 | 
            +
                                strategy_id: {
         | 
| 573 | 
            +
                                    "matrix": strategy,
         | 
| 574 | 
            +
                                    "stages": context.pop("stages", {}),
         | 
| 575 | 
            +
                                    "errors": JobException(error_msg).to_dict(),
         | 
| 576 | 
            +
                                },
         | 
| 577 | 
            +
                            },
         | 
| 578 | 
            +
                        )
         | 
| 491 579 |  | 
| 492 580 | 
             
                    # PARAGRAPH:
         | 
| 493 581 | 
             
                    #
         | 
| 494 | 
            -
                    #        | 
| 495 | 
            -
                    #    | 
| 582 | 
            +
                    #       This step will add the stage result to `stages` key in that
         | 
| 583 | 
            +
                    #   stage id. It will have structure like;
         | 
| 496 584 | 
             
                    #
         | 
| 497 585 | 
             
                    #   {
         | 
| 498 | 
            -
                    #       "params": { ... }, | 
| 499 | 
            -
                    #       "jobs": { ... }, | 
| 500 | 
            -
                    #       "matrix": { ... } | 
| 501 | 
            -
                    #       "stages": { ... } | 
| 586 | 
            +
                    #       "params": { ... },
         | 
| 587 | 
            +
                    #       "jobs": { ... },
         | 
| 588 | 
            +
                    #       "matrix": { ... },
         | 
| 589 | 
            +
                    #       "stages": { { "stage-id-01": { "outputs": { ... } } }, ... }
         | 
| 502 590 | 
             
                    #   }
         | 
| 503 591 | 
             
                    #
         | 
| 504 | 
            -
                     | 
| 505 | 
            -
                     | 
| 506 | 
            -
             | 
| 507 | 
            -
                    #  | 
| 508 | 
            -
                     | 
| 592 | 
            +
                    # IMPORTANT:
         | 
| 593 | 
            +
                    #   This execution change all stage running IDs to the current job
         | 
| 594 | 
            +
                    #   running ID, but it still trac log to the same parent running ID
         | 
| 595 | 
            +
                    #   (with passing `run_id` and `parent_run_id` to the stage
         | 
| 596 | 
            +
                    #   execution arguments).
         | 
| 597 | 
            +
                    #
         | 
| 598 | 
            +
                    try:
         | 
| 599 | 
            +
                        stage.set_outputs(
         | 
| 600 | 
            +
                            stage.handler_execute(
         | 
| 601 | 
            +
                                params=context,
         | 
| 602 | 
            +
                                run_id=result.run_id,
         | 
| 603 | 
            +
                                parent_run_id=result.parent_run_id,
         | 
| 604 | 
            +
                            ).context,
         | 
| 605 | 
            +
                            to=context,
         | 
| 606 | 
            +
                        )
         | 
| 607 | 
            +
                    except (StageException, UtilException) as err:
         | 
| 608 | 
            +
                        result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
         | 
| 609 | 
            +
                        if raise_error or config.job_raise_error:
         | 
| 610 | 
            +
                            raise JobException(
         | 
| 611 | 
            +
                                f"Stage execution error: {err.__class__.__name__}: "
         | 
| 612 | 
            +
                                f"{err}"
         | 
| 613 | 
            +
                            ) from None
         | 
| 614 | 
            +
             | 
| 615 | 
            +
                        return result.catch(
         | 
| 616 | 
            +
                            status=Status.FAILED,
         | 
| 617 | 
            +
                            context={
         | 
| 618 | 
            +
                                strategy_id: {
         | 
| 619 | 
            +
                                    "matrix": strategy,
         | 
| 620 | 
            +
                                    "stages": context.pop("stages", {}),
         | 
| 621 | 
            +
                                    "errors": err.to_dict(),
         | 
| 622 | 
            +
                                },
         | 
| 623 | 
            +
                            },
         | 
| 624 | 
            +
                        )
         | 
| 509 625 |  | 
| 510 | 
            -
             | 
| 511 | 
            -
             | 
| 512 | 
            -
                            continue
         | 
| 626 | 
            +
                    # NOTE: Remove the current stage object for saving memory.
         | 
| 627 | 
            +
                    del stage
         | 
| 513 628 |  | 
| 514 | 
            -
             | 
| 629 | 
            +
                return result.catch(
         | 
| 630 | 
            +
                    status=Status.SUCCESS,
         | 
| 631 | 
            +
                    context={
         | 
| 632 | 
            +
                        strategy_id: {
         | 
| 633 | 
            +
                            "matrix": strategy,
         | 
| 634 | 
            +
                            "stages": filter_func(context.pop("stages", {})),
         | 
| 635 | 
            +
                        },
         | 
| 636 | 
            +
                    },
         | 
| 637 | 
            +
                )
         | 
| 515 638 |  | 
| 516 | 
            -
                        # NOTE: Logging a matrix that pass on this stage execution.
         | 
| 517 | 
            -
                        if strategy:
         | 
| 518 | 
            -
                            result.trace.info(f"[JOB]: ... Matrix: {strategy}")
         | 
| 519 639 |  | 
| 520 | 
            -
             | 
| 521 | 
            -
             | 
| 522 | 
            -
             | 
| 523 | 
            -
             | 
| 524 | 
            -
             | 
| 525 | 
            -
             | 
| 526 | 
            -
             | 
| 527 | 
            -
             | 
| 528 | 
            -
             | 
| 529 | 
            -
             | 
| 530 | 
            -
             | 
| 531 | 
            -
             | 
| 532 | 
            -
             | 
| 533 | 
            -
             | 
| 534 | 
            -
             | 
| 535 | 
            -
             | 
| 536 | 
            -
             | 
| 537 | 
            -
             | 
| 538 | 
            -
             | 
| 539 | 
            -
             | 
| 540 | 
            -
             | 
| 541 | 
            -
             | 
| 542 | 
            -
             | 
| 543 | 
            -
             | 
| 544 | 
            -
             | 
| 545 | 
            -
             | 
| 640 | 
            +
            def local_execute(
         | 
| 641 | 
            +
                job: Job,
         | 
| 642 | 
            +
                params: DictData,
         | 
| 643 | 
            +
                *,
         | 
| 644 | 
            +
                run_id: str | None = None,
         | 
| 645 | 
            +
                parent_run_id: str | None = None,
         | 
| 646 | 
            +
                result: Result | None = None,
         | 
| 647 | 
            +
                event: Event | None = None,
         | 
| 648 | 
            +
                raise_error: bool = False,
         | 
| 649 | 
            +
            ) -> Result:
         | 
| 650 | 
            +
                """Local job execution with passing dynamic parameters from the workflow
         | 
| 651 | 
            +
                execution. It will generate matrix values at the first step and run
         | 
| 652 | 
            +
                multithread on this metrics to the `stages` field of this job.
         | 
| 653 | 
            +
             | 
| 654 | 
            +
                    This method does not raise any JobException if it runs with
         | 
| 655 | 
            +
                multi-threading strategy.
         | 
| 656 | 
            +
             | 
| 657 | 
            +
                :param job: (Job) A job model that want to execute.
         | 
| 658 | 
            +
                :param params: (DictData) An input parameters that use on job execution.
         | 
| 659 | 
            +
                :param run_id: (str) A job running ID for this execution.
         | 
| 660 | 
            +
                :param parent_run_id: (str) A parent workflow running ID for this release.
         | 
| 661 | 
            +
                :param result: (Result) A result object for keeping context and status
         | 
| 662 | 
            +
                    data.
         | 
| 663 | 
            +
                :param event: (Event) An event manager that pass to the PoolThreadExecutor.
         | 
| 664 | 
            +
                :param raise_error: (bool) A flag that all this method raise error to the
         | 
| 665 | 
            +
                    strategy execution.
         | 
| 666 | 
            +
             | 
| 667 | 
            +
                :rtype: Result
         | 
| 668 | 
            +
                """
         | 
| 669 | 
            +
                result: Result = Result.construct_with_rs_or_id(
         | 
| 670 | 
            +
                    result,
         | 
| 671 | 
            +
                    run_id=run_id,
         | 
| 672 | 
            +
                    parent_run_id=parent_run_id,
         | 
| 673 | 
            +
                    id_logic=(job.id or "not-set"),
         | 
| 674 | 
            +
                )
         | 
| 675 | 
            +
                event: Event = Event() if event is None else event
         | 
| 676 | 
            +
             | 
| 677 | 
            +
                # NOTE: Normal Job execution without parallel strategy matrix. It uses
         | 
| 678 | 
            +
                #   for-loop to control strategy execution sequentially.
         | 
| 679 | 
            +
                if (not job.strategy.is_set()) or job.strategy.max_parallel == 1:
         | 
| 680 | 
            +
             | 
| 681 | 
            +
                    for strategy in job.strategy.make():
         | 
| 682 | 
            +
             | 
| 683 | 
            +
                        # TODO: stop and raise error if the event was set.
         | 
| 684 | 
            +
                        local_execute_strategy(
         | 
| 685 | 
            +
                            job=job,
         | 
| 686 | 
            +
                            strategy=strategy,
         | 
| 687 | 
            +
                            params=params,
         | 
| 688 | 
            +
                            result=result,
         | 
| 689 | 
            +
                            event=event,
         | 
| 690 | 
            +
                            raise_error=raise_error,
         | 
| 691 | 
            +
                        )
         | 
| 546 692 |  | 
| 547 | 
            -
             | 
| 548 | 
            -
                        #
         | 
| 549 | 
            -
                        #       I do not use below syntax because `params` dict be the
         | 
| 550 | 
            -
                        #   reference memory pointer, and it was changed when I action
         | 
| 551 | 
            -
                        #   anything like update or re-construct this.
         | 
| 552 | 
            -
                        #
         | 
| 553 | 
            -
                        #       ... params |= stage.execute(params=params)
         | 
| 554 | 
            -
                        #
         | 
| 555 | 
            -
                        #   This step will add the stage result to `stages` key in
         | 
| 556 | 
            -
                        #   that stage id. It will have structure like;
         | 
| 557 | 
            -
                        #
         | 
| 558 | 
            -
                        #   {
         | 
| 559 | 
            -
                        #       "params": { ... },
         | 
| 560 | 
            -
                        #       "jobs": { ... },
         | 
| 561 | 
            -
                        #       "matrix": { ... },
         | 
| 562 | 
            -
                        #       "stages": { { "stage-id-1": ... }, ... }
         | 
| 563 | 
            -
                        #   }
         | 
| 564 | 
            -
                        #
         | 
| 565 | 
            -
                        # IMPORTANT:
         | 
| 566 | 
            -
                        #   This execution change all stage running IDs to the current job
         | 
| 567 | 
            -
                        #   running ID, but it still trac log to the same parent running ID
         | 
| 568 | 
            -
                        #   (with passing `run_id` and `parent_run_id` to the stage
         | 
| 569 | 
            -
                        #   execution arguments).
         | 
| 570 | 
            -
                        #
         | 
| 571 | 
            -
                        try:
         | 
| 572 | 
            -
                            stage.set_outputs(
         | 
| 573 | 
            -
                                stage.handler_execute(
         | 
| 574 | 
            -
                                    params=context,
         | 
| 575 | 
            -
                                    run_id=result.run_id,
         | 
| 576 | 
            -
                                    parent_run_id=result.parent_run_id,
         | 
| 577 | 
            -
                                ).context,
         | 
| 578 | 
            -
                                to=context,
         | 
| 579 | 
            -
                            )
         | 
| 580 | 
            -
                        except (StageException, UtilException) as err:
         | 
| 581 | 
            -
                            result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
         | 
| 582 | 
            -
                            if config.job_raise_error:
         | 
| 583 | 
            -
                                raise JobException(
         | 
| 584 | 
            -
                                    f"Stage execution error: {err.__class__.__name__}: "
         | 
| 585 | 
            -
                                    f"{err}"
         | 
| 586 | 
            -
                                ) from None
         | 
| 587 | 
            -
             | 
| 588 | 
            -
                            return result.catch(
         | 
| 589 | 
            -
                                status=1,
         | 
| 590 | 
            -
                                context={
         | 
| 591 | 
            -
                                    strategy_id: {
         | 
| 592 | 
            -
                                        "matrix": strategy,
         | 
| 593 | 
            -
                                        "stages": context.pop("stages", {}),
         | 
| 594 | 
            -
                                        "errors": {
         | 
| 595 | 
            -
                                            "class": err,
         | 
| 596 | 
            -
                                            "name": err.__class__.__name__,
         | 
| 597 | 
            -
                                            "message": f"{err.__class__.__name__}: {err}",
         | 
| 598 | 
            -
                                        },
         | 
| 599 | 
            -
                                    },
         | 
| 600 | 
            -
                                },
         | 
| 601 | 
            -
                            )
         | 
| 693 | 
            +
                    return result.catch(status=Status.SUCCESS)
         | 
| 602 694 |  | 
| 603 | 
            -
             | 
| 604 | 
            -
             | 
| 695 | 
            +
                fail_fast_flag: bool = job.strategy.fail_fast
         | 
| 696 | 
            +
                ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
         | 
| 605 697 |  | 
| 606 | 
            -
             | 
| 607 | 
            -
             | 
| 608 | 
            -
             | 
| 609 | 
            -
             | 
| 610 | 
            -
                                "matrix": strategy,
         | 
| 611 | 
            -
                                "stages": filter_func(context.pop("stages", {})),
         | 
| 612 | 
            -
                            },
         | 
| 613 | 
            -
                        },
         | 
| 614 | 
            -
                    )
         | 
| 698 | 
            +
                result.trace.info(
         | 
| 699 | 
            +
                    f"[JOB]: Start multithreading: {job.strategy.max_parallel} threads "
         | 
| 700 | 
            +
                    f"with {ls} mode."
         | 
| 701 | 
            +
                )
         | 
| 615 702 |  | 
| 616 | 
            -
                 | 
| 617 | 
            -
             | 
| 618 | 
            -
             | 
| 619 | 
            -
                     | 
| 620 | 
            -
                     | 
| 621 | 
            -
             | 
| 622 | 
            -
             | 
| 623 | 
            -
             | 
| 624 | 
            -
             | 
| 625 | 
            -
             | 
| 626 | 
            -
             | 
| 703 | 
            +
                # IMPORTANT: Start running strategy execution by multithreading because
         | 
| 704 | 
            +
                #   it will run by strategy values without waiting previous execution.
         | 
| 705 | 
            +
                with ThreadPoolExecutor(
         | 
| 706 | 
            +
                    max_workers=job.strategy.max_parallel,
         | 
| 707 | 
            +
                    thread_name_prefix="job_strategy_exec_",
         | 
| 708 | 
            +
                ) as executor:
         | 
| 709 | 
            +
             | 
| 710 | 
            +
                    futures: list[Future] = [
         | 
| 711 | 
            +
                        executor.submit(
         | 
| 712 | 
            +
                            local_execute_strategy,
         | 
| 713 | 
            +
                            job=job,
         | 
| 714 | 
            +
                            strategy=strategy,
         | 
| 715 | 
            +
                            params=params,
         | 
| 716 | 
            +
                            result=result,
         | 
| 717 | 
            +
                            event=event,
         | 
| 718 | 
            +
                            raise_error=raise_error,
         | 
| 719 | 
            +
                        )
         | 
| 720 | 
            +
                        for strategy in job.strategy.make()
         | 
| 721 | 
            +
                    ]
         | 
| 627 722 |  | 
| 628 | 
            -
                    : | 
| 629 | 
            -
                    : | 
| 630 | 
            -
                    :param parent_run_id: A parent workflow running ID for this release.
         | 
| 631 | 
            -
                    :param result: (Result) A result object for keeping context and status
         | 
| 632 | 
            -
                        data.
         | 
| 723 | 
            +
                    context: DictData = {}
         | 
| 724 | 
            +
                    status: Status = Status.SUCCESS
         | 
| 633 725 |  | 
| 634 | 
            -
                    : | 
| 635 | 
            -
             | 
| 636 | 
            -
                     | 
| 637 | 
            -
                         | 
| 638 | 
            -
                             | 
| 639 | 
            -
                            parent_run_id=parent_run_id,
         | 
| 726 | 
            +
                    if not fail_fast_flag:
         | 
| 727 | 
            +
                        done = as_completed(futures, timeout=1800)
         | 
| 728 | 
            +
                    else:
         | 
| 729 | 
            +
                        done, not_done = wait(
         | 
| 730 | 
            +
                            futures, timeout=1800, return_when=FIRST_EXCEPTION
         | 
| 640 731 | 
             
                        )
         | 
| 641 | 
            -
                    elif parent_run_id:  # pragma: no cov
         | 
| 642 | 
            -
                        result.set_parent_run_id(parent_run_id)
         | 
| 643 | 
            -
             | 
| 644 | 
            -
                    # NOTE: Normal Job execution without parallel strategy matrix. It uses
         | 
| 645 | 
            -
                    #   for-loop to control strategy execution sequentially.
         | 
| 646 | 
            -
                    if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
         | 
| 647 | 
            -
             | 
| 648 | 
            -
                        for strategy in self.strategy.make():
         | 
| 649 | 
            -
                            result: Result = self.execute_strategy(
         | 
| 650 | 
            -
                                strategy=strategy,
         | 
| 651 | 
            -
                                params=params,
         | 
| 652 | 
            -
                                result=result,
         | 
| 653 | 
            -
                            )
         | 
| 654 732 |  | 
| 655 | 
            -
                         | 
| 656 | 
            -
             | 
| 657 | 
            -
             | 
| 658 | 
            -
                    event: Event = Event()
         | 
| 659 | 
            -
             | 
| 660 | 
            -
                    # IMPORTANT: Start running strategy execution by multithreading because
         | 
| 661 | 
            -
                    #   it will run by strategy values without waiting previous execution.
         | 
| 662 | 
            -
                    with ThreadPoolExecutor(
         | 
| 663 | 
            -
                        max_workers=self.strategy.max_parallel,
         | 
| 664 | 
            -
                        thread_name_prefix="job_strategy_exec_",
         | 
| 665 | 
            -
                    ) as executor:
         | 
| 666 | 
            -
             | 
| 667 | 
            -
                        futures: list[Future] = [
         | 
| 668 | 
            -
                            executor.submit(
         | 
| 669 | 
            -
                                self.execute_strategy,
         | 
| 670 | 
            -
                                strategy=strategy,
         | 
| 671 | 
            -
                                params=params,
         | 
| 672 | 
            -
                                result=result,
         | 
| 673 | 
            -
                                event=event,
         | 
| 674 | 
            -
                            )
         | 
| 675 | 
            -
                            for strategy in self.strategy.make()
         | 
| 676 | 
            -
                        ]
         | 
| 677 | 
            -
             | 
| 678 | 
            -
                        context: DictData = {}
         | 
| 679 | 
            -
                        status: Status = Status.SUCCESS
         | 
| 680 | 
            -
                        fail_fast_flag: bool = self.strategy.fail_fast
         | 
| 681 | 
            -
             | 
| 682 | 
            -
                        if fail_fast_flag:
         | 
| 683 | 
            -
                            # NOTE: Get results from a collection of tasks with a timeout
         | 
| 684 | 
            -
                            #   that has the first exception.
         | 
| 685 | 
            -
                            done, not_done = wait(
         | 
| 686 | 
            -
                                futures, timeout=1800, return_when=FIRST_EXCEPTION
         | 
| 733 | 
            +
                        if len(done) != len(futures):
         | 
| 734 | 
            +
                            result.trace.warning(
         | 
| 735 | 
            +
                                "[JOB]: Set the event for stop running stage."
         | 
| 687 736 | 
             
                            )
         | 
| 688 | 
            -
                             | 
| 689 | 
            -
             | 
| 690 | 
            -
                                 | 
| 691 | 
            -
             | 
| 737 | 
            +
                            event.set()
         | 
| 738 | 
            +
                            for future in not_done:
         | 
| 739 | 
            +
                                future.cancel()
         | 
| 740 | 
            +
             | 
| 741 | 
            +
                        nd: str = (
         | 
| 742 | 
            +
                            f", the strategies do not run is {not_done}" if not_done else ""
         | 
| 743 | 
            +
                        )
         | 
| 744 | 
            +
                        result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
         | 
| 745 | 
            +
             | 
| 746 | 
            +
                    for future in done:
         | 
| 747 | 
            +
                        try:
         | 
| 748 | 
            +
                            future.result()
         | 
| 749 | 
            +
                        except JobException as err:
         | 
| 750 | 
            +
                            status = Status.FAILED
         | 
| 751 | 
            +
                            result.trace.error(
         | 
| 752 | 
            +
                                f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
         | 
| 753 | 
            +
                                f"\n\t{err}"
         | 
| 692 754 | 
             
                            )
         | 
| 693 | 
            -
                             | 
| 694 | 
            -
             | 
| 695 | 
            -
             | 
| 696 | 
            -
                            #   and cancel any scheduled tasks.
         | 
| 697 | 
            -
                            if len(done) != len(futures):
         | 
| 698 | 
            -
                                event.set()
         | 
| 699 | 
            -
                                for future in not_done:
         | 
| 700 | 
            -
                                    future.cancel()
         | 
| 701 | 
            -
                        else:
         | 
| 702 | 
            -
                            done = as_completed(futures, timeout=1800)
         | 
| 703 | 
            -
             | 
| 704 | 
            -
                        for future in done:
         | 
| 705 | 
            -
                            try:
         | 
| 706 | 
            -
                                future.result()
         | 
| 707 | 
            -
                            except JobException as err:
         | 
| 708 | 
            -
                                status = Status.FAILED
         | 
| 709 | 
            -
                                ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
         | 
| 710 | 
            -
                                result.trace.error(
         | 
| 711 | 
            -
                                    f"[JOB]: {ls} Catch:\n\t{err.__class__.__name__}:"
         | 
| 712 | 
            -
                                    f"\n\t{err}"
         | 
| 713 | 
            -
                                )
         | 
| 714 | 
            -
                                context.update(
         | 
| 715 | 
            -
                                    {
         | 
| 716 | 
            -
                                        "errors": {
         | 
| 717 | 
            -
                                            "class": err,
         | 
| 718 | 
            -
                                            "name": err.__class__.__name__,
         | 
| 719 | 
            -
                                            "message": f"{err.__class__.__name__}: {err}",
         | 
| 720 | 
            -
                                        },
         | 
| 721 | 
            -
                                    },
         | 
| 722 | 
            -
                                )
         | 
| 723 | 
            -
             | 
| 724 | 
            -
                    return result.catch(status=status, context=context)
         | 
| 755 | 
            +
                            context.update({"errors": err.to_dict()})
         | 
| 756 | 
            +
             | 
| 757 | 
            +
                return result.catch(status=status, context=context)
         |