ddeutil-workflow 0.0.41__py3-none-any.whl → 0.0.42__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/api/api.py +7 -7
- ddeutil/workflow/api/routes/schedules.py +5 -5
- ddeutil/workflow/api/routes/workflows.py +2 -2
- ddeutil/workflow/conf.py +39 -28
- ddeutil/workflow/cron.py +12 -13
- ddeutil/workflow/job.py +18 -10
- ddeutil/workflow/logs.py +33 -6
- ddeutil/workflow/reusables.py +16 -13
- ddeutil/workflow/scheduler.py +26 -28
- ddeutil/workflow/stages.py +257 -62
- ddeutil/workflow/utils.py +0 -1
- ddeutil/workflow/workflow.py +111 -74
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.42.dist-info}/METADATA +6 -9
- ddeutil_workflow-0.0.42.dist-info/RECORD +30 -0
- ddeutil/workflow/context.py +0 -61
- ddeutil_workflow-0.0.41.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.42.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.41.dist-info → ddeutil_workflow-0.0.42.dist-info}/top_level.txt +0 -0
    
        ddeutil/workflow/stages.py
    CHANGED
    
    | @@ -3,7 +3,7 @@ | |
| 3 3 | 
             
            # Licensed under the MIT License. See LICENSE in the project root for
         | 
| 4 4 | 
             
            # license information.
         | 
| 5 5 | 
             
            # ------------------------------------------------------------------------------
         | 
| 6 | 
            -
            # [x] Use config
         | 
| 6 | 
            +
            # [x] Use dynamic config
         | 
| 7 7 | 
             
            """Stage Model that use for getting stage data template from the Job Model.
         | 
| 8 8 | 
             
            The stage handle the minimize task that run in some thread (same thread at
         | 
| 9 9 | 
             
            its job owner) that mean it is the lowest executor of a workflow that can
         | 
| @@ -50,7 +50,7 @@ from pydantic.functional_validators import model_validator | |
| 50 50 | 
             
            from typing_extensions import Self
         | 
| 51 51 |  | 
| 52 52 | 
             
            from .__types import DictData, DictStr, TupleStr
         | 
| 53 | 
            -
            from .conf import  | 
| 53 | 
            +
            from .conf import dynamic
         | 
| 54 54 | 
             
            from .exceptions import StageException, to_dict
         | 
| 55 55 | 
             
            from .result import Result, Status
         | 
| 56 56 | 
             
            from .reusables import TagFunc, extract_call, not_in_template, param2template
         | 
| @@ -67,6 +67,7 @@ __all__: TupleStr = ( | |
| 67 67 | 
             
                "TriggerStage",
         | 
| 68 68 | 
             
                "ForEachStage",
         | 
| 69 69 | 
             
                "ParallelStage",
         | 
| 70 | 
            +
                "RaiseStage",
         | 
| 70 71 | 
             
                "Stage",
         | 
| 71 72 | 
             
            )
         | 
| 72 73 |  | 
| @@ -96,7 +97,7 @@ class BaseStage(BaseModel, ABC): | |
| 96 97 | 
             
                )
         | 
| 97 98 | 
             
                extras: DictData = Field(
         | 
| 98 99 | 
             
                    default_factory=dict,
         | 
| 99 | 
            -
                    description="An extra override values.",
         | 
| 100 | 
            +
                    description="An extra override config values.",
         | 
| 100 101 | 
             
                )
         | 
| 101 102 |  | 
| 102 103 | 
             
                @property
         | 
| @@ -151,15 +152,6 @@ class BaseStage(BaseModel, ABC): | |
| 151 152 | 
             
                    """
         | 
| 152 153 | 
             
                    raise NotImplementedError("Stage should implement `execute` method.")
         | 
| 153 154 |  | 
| 154 | 
            -
                async def axecute(
         | 
| 155 | 
            -
                    self,
         | 
| 156 | 
            -
                    params: DictData,
         | 
| 157 | 
            -
                    *,
         | 
| 158 | 
            -
                    result: Result | None = None,
         | 
| 159 | 
            -
                    event: Event | None,
         | 
| 160 | 
            -
                ) -> Result:  # pragma: no cov
         | 
| 161 | 
            -
                    ...
         | 
| 162 | 
            -
             | 
| 163 155 | 
             
                def handler_execute(
         | 
| 164 156 | 
             
                    self,
         | 
| 165 157 | 
             
                    params: DictData,
         | 
| @@ -167,7 +159,7 @@ class BaseStage(BaseModel, ABC): | |
| 167 159 | 
             
                    run_id: str | None = None,
         | 
| 168 160 | 
             
                    parent_run_id: str | None = None,
         | 
| 169 161 | 
             
                    result: Result | None = None,
         | 
| 170 | 
            -
                    raise_error: bool =  | 
| 162 | 
            +
                    raise_error: bool | None = None,
         | 
| 171 163 | 
             
                    to: DictData | None = None,
         | 
| 172 164 | 
             
                    event: Event | None = None,
         | 
| 173 165 | 
             
                ) -> Result:
         | 
| @@ -223,7 +215,7 @@ class BaseStage(BaseModel, ABC): | |
| 223 215 | 
             
                    except Exception as err:
         | 
| 224 216 | 
             
                        result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
         | 
| 225 217 |  | 
| 226 | 
            -
                        if raise_error  | 
| 218 | 
            +
                        if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
         | 
| 227 219 | 
             
                            if isinstance(err, StageException):
         | 
| 228 220 | 
             
                                raise
         | 
| 229 221 |  | 
| @@ -268,13 +260,17 @@ class BaseStage(BaseModel, ABC): | |
| 268 260 | 
             
                    if "stages" not in to:
         | 
| 269 261 | 
             
                        to["stages"] = {}
         | 
| 270 262 |  | 
| 271 | 
            -
                    if self.id is None and not  | 
| 263 | 
            +
                    if self.id is None and not dynamic(
         | 
| 264 | 
            +
                        "stage_default_id", extras=self.extras
         | 
| 265 | 
            +
                    ):
         | 
| 272 266 | 
             
                        return to
         | 
| 273 267 |  | 
| 274 268 | 
             
                    _id: str = (
         | 
| 275 | 
            -
                        param2template(self.id, params=to)
         | 
| 269 | 
            +
                        param2template(self.id, params=to, extras=self.extras)
         | 
| 276 270 | 
             
                        if self.id
         | 
| 277 | 
            -
                        else gen_id( | 
| 271 | 
            +
                        else gen_id(
         | 
| 272 | 
            +
                            param2template(self.name, params=to, extras=self.extras)
         | 
| 273 | 
            +
                        )
         | 
| 278 274 | 
             
                    )
         | 
| 279 275 |  | 
| 280 276 | 
             
                    errors: DictData = (
         | 
| @@ -312,7 +308,9 @@ class BaseStage(BaseModel, ABC): | |
| 312 308 | 
             
                        #   should use the `re` module to validate eval-string before
         | 
| 313 309 | 
             
                        #   running.
         | 
| 314 310 | 
             
                        rs: bool = eval(
         | 
| 315 | 
            -
                            param2template(self.condition, params | 
| 311 | 
            +
                            param2template(self.condition, params, extras=self.extras),
         | 
| 312 | 
            +
                            globals() | params,
         | 
| 313 | 
            +
                            {},
         | 
| 316 314 | 
             
                        )
         | 
| 317 315 | 
             
                        if not isinstance(rs, bool):
         | 
| 318 316 | 
             
                            raise TypeError("Return type of condition does not be boolean")
         | 
| @@ -321,7 +319,102 @@ class BaseStage(BaseModel, ABC): | |
| 321 319 | 
             
                        raise StageException(f"{err.__class__.__name__}: {err}") from err
         | 
| 322 320 |  | 
| 323 321 |  | 
| 324 | 
            -
            class  | 
| 322 | 
            +
            class BaseAsyncStage(BaseStage):
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                @abstractmethod
         | 
| 325 | 
            +
                def execute(
         | 
| 326 | 
            +
                    self,
         | 
| 327 | 
            +
                    params: DictData,
         | 
| 328 | 
            +
                    *,
         | 
| 329 | 
            +
                    result: Result | None = None,
         | 
| 330 | 
            +
                    event: Event | None = None,
         | 
| 331 | 
            +
                ) -> Result: ...
         | 
| 332 | 
            +
             | 
| 333 | 
            +
                @abstractmethod
         | 
| 334 | 
            +
                async def axecute(
         | 
| 335 | 
            +
                    self,
         | 
| 336 | 
            +
                    params: DictData,
         | 
| 337 | 
            +
                    *,
         | 
| 338 | 
            +
                    result: Result | None = None,
         | 
| 339 | 
            +
                    event: Event | None = None,
         | 
| 340 | 
            +
                ) -> Result:
         | 
| 341 | 
            +
                    """Async execution method for this Empty stage that only logging out to
         | 
| 342 | 
            +
                    stdout.
         | 
| 343 | 
            +
             | 
| 344 | 
            +
                    :param params: (DictData) A context data that want to add output result.
         | 
| 345 | 
            +
                        But this stage does not pass any output.
         | 
| 346 | 
            +
                    :param result: (Result) A result object for keeping context and status
         | 
| 347 | 
            +
                        data.
         | 
| 348 | 
            +
                    :param event: (Event) An event manager that use to track parent execute
         | 
| 349 | 
            +
                        was not force stopped.
         | 
| 350 | 
            +
             | 
| 351 | 
            +
                    :rtype: Result
         | 
| 352 | 
            +
                    """
         | 
| 353 | 
            +
                    raise NotImplementedError(
         | 
| 354 | 
            +
                        "Async Stage should implement `axecute` method."
         | 
| 355 | 
            +
                    )
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                async def handler_axecute(
         | 
| 358 | 
            +
                    self,
         | 
| 359 | 
            +
                    params: DictData,
         | 
| 360 | 
            +
                    *,
         | 
| 361 | 
            +
                    run_id: str | None = None,
         | 
| 362 | 
            +
                    parent_run_id: str | None = None,
         | 
| 363 | 
            +
                    result: Result | None = None,
         | 
| 364 | 
            +
                    raise_error: bool | None = None,
         | 
| 365 | 
            +
                    to: DictData | None = None,
         | 
| 366 | 
            +
                    event: Event | None = None,
         | 
| 367 | 
            +
                ) -> Result:
         | 
| 368 | 
            +
                    """Async Handler stage execution result from the stage `execute` method.
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                    :param params: (DictData) A parameterize value data that use in this
         | 
| 371 | 
            +
                        stage execution.
         | 
| 372 | 
            +
                    :param run_id: (str) A running stage ID for this execution.
         | 
| 373 | 
            +
                    :param parent_run_id: (str) A parent workflow running ID for this
         | 
| 374 | 
            +
                        execution.
         | 
| 375 | 
            +
                    :param result: (Result) A result object for keeping context and status
         | 
| 376 | 
            +
                        data before execution.
         | 
| 377 | 
            +
                    :param raise_error: (bool) A flag that all this method raise error
         | 
| 378 | 
            +
                    :param to: (DictData) A target object for auto set the return output
         | 
| 379 | 
            +
                        after execution.
         | 
| 380 | 
            +
                    :param event: (Event) An event manager that pass to the stage execution.
         | 
| 381 | 
            +
             | 
| 382 | 
            +
                    :rtype: Result
         | 
| 383 | 
            +
                    """
         | 
| 384 | 
            +
                    result: Result = Result.construct_with_rs_or_id(
         | 
| 385 | 
            +
                        result,
         | 
| 386 | 
            +
                        run_id=run_id,
         | 
| 387 | 
            +
                        parent_run_id=parent_run_id,
         | 
| 388 | 
            +
                        id_logic=self.iden,
         | 
| 389 | 
            +
                    )
         | 
| 390 | 
            +
             | 
| 391 | 
            +
                    try:
         | 
| 392 | 
            +
                        rs: Result = await self.axecute(params, result=result, event=event)
         | 
| 393 | 
            +
                        if to is not None:
         | 
| 394 | 
            +
                            return self.set_outputs(rs.context, to=to)
         | 
| 395 | 
            +
                        return rs
         | 
| 396 | 
            +
                    except Exception as err:
         | 
| 397 | 
            +
                        await result.trace.aerror(
         | 
| 398 | 
            +
                            f"[STAGE]: {err.__class__.__name__}: {err}"
         | 
| 399 | 
            +
                        )
         | 
| 400 | 
            +
             | 
| 401 | 
            +
                        if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
         | 
| 402 | 
            +
                            if isinstance(err, StageException):
         | 
| 403 | 
            +
                                raise
         | 
| 404 | 
            +
             | 
| 405 | 
            +
                            raise StageException(
         | 
| 406 | 
            +
                                f"{self.__class__.__name__}: \n\t"
         | 
| 407 | 
            +
                                f"{err.__class__.__name__}: {err}"
         | 
| 408 | 
            +
                            ) from None
         | 
| 409 | 
            +
             | 
| 410 | 
            +
                        errors: DictData = {"errors": to_dict(err)}
         | 
| 411 | 
            +
                        if to is not None:
         | 
| 412 | 
            +
                            return self.set_outputs(errors, to=to)
         | 
| 413 | 
            +
             | 
| 414 | 
            +
                        return result.catch(status=Status.FAILED, context=errors)
         | 
| 415 | 
            +
             | 
| 416 | 
            +
             | 
| 417 | 
            +
            class EmptyStage(BaseAsyncStage):
         | 
| 325 418 | 
             
                """Empty stage that do nothing (context equal empty stage) and logging the
         | 
| 326 419 | 
             
                name of stage only to stdout.
         | 
| 327 420 |  | 
| @@ -372,7 +465,7 @@ class EmptyStage(BaseStage): | |
| 372 465 |  | 
| 373 466 | 
             
                    result.trace.info(
         | 
| 374 467 | 
             
                        f"[STAGE]: Empty-Execute: {self.name!r}: "
         | 
| 375 | 
            -
                        f"( {param2template(self.echo, params= | 
| 468 | 
            +
                        f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
         | 
| 376 469 | 
             
                    )
         | 
| 377 470 | 
             
                    if self.sleep > 0:
         | 
| 378 471 | 
             
                        if self.sleep > 5:
         | 
| @@ -381,14 +474,13 @@ class EmptyStage(BaseStage): | |
| 381 474 |  | 
| 382 475 | 
             
                    return result.catch(status=Status.SUCCESS)
         | 
| 383 476 |  | 
| 384 | 
            -
                # TODO: Draft async execute method for the perf improvement.
         | 
| 385 477 | 
             
                async def axecute(
         | 
| 386 478 | 
             
                    self,
         | 
| 387 479 | 
             
                    params: DictData,
         | 
| 388 480 | 
             
                    *,
         | 
| 389 481 | 
             
                    result: Result | None = None,
         | 
| 390 | 
            -
                    event: Event | None,
         | 
| 391 | 
            -
                ) -> Result: | 
| 482 | 
            +
                    event: Event | None = None,
         | 
| 483 | 
            +
                ) -> Result:
         | 
| 392 484 | 
             
                    """Async execution method for this Empty stage that only logging out to
         | 
| 393 485 | 
             
                    stdout.
         | 
| 394 486 |  | 
| @@ -406,14 +498,16 @@ class EmptyStage(BaseStage): | |
| 406 498 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 407 499 | 
             
                        )
         | 
| 408 500 |  | 
| 409 | 
            -
                    result.trace. | 
| 501 | 
            +
                    await result.trace.ainfo(
         | 
| 410 502 | 
             
                        f"[STAGE]: Empty-Execute: {self.name!r}: "
         | 
| 411 | 
            -
                        f"( {param2template(self.echo, params= | 
| 503 | 
            +
                        f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
         | 
| 412 504 | 
             
                    )
         | 
| 413 505 |  | 
| 414 506 | 
             
                    if self.sleep > 0:
         | 
| 415 507 | 
             
                        if self.sleep > 5:
         | 
| 416 | 
            -
                            result.trace. | 
| 508 | 
            +
                            await result.trace.ainfo(
         | 
| 509 | 
            +
                                f"[STAGE]: ... sleep ({self.sleep} seconds)"
         | 
| 510 | 
            +
                            )
         | 
| 417 511 | 
             
                        await asyncio.sleep(self.sleep)
         | 
| 418 512 |  | 
| 419 513 | 
             
                    return result.catch(status=Status.SUCCESS)
         | 
| @@ -508,12 +602,14 @@ class BashStage(BaseStage): | |
| 508 602 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 509 603 | 
             
                        )
         | 
| 510 604 |  | 
| 511 | 
            -
                    bash: str = param2template( | 
| 605 | 
            +
                    bash: str = param2template(
         | 
| 606 | 
            +
                        dedent(self.bash), params, extras=self.extras
         | 
| 607 | 
            +
                    )
         | 
| 512 608 |  | 
| 513 609 | 
             
                    result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
         | 
| 514 610 | 
             
                    with self.create_sh_file(
         | 
| 515 611 | 
             
                        bash=bash,
         | 
| 516 | 
            -
                        env=param2template(self.env, params),
         | 
| 612 | 
            +
                        env=param2template(self.env, params, extras=self.extras),
         | 
| 517 613 | 
             
                        run_id=result.run_id,
         | 
| 518 614 | 
             
                    ) as sh:
         | 
| 519 615 | 
             
                        result.trace.debug(f"... Start create `{sh[1]}` file.")
         | 
| @@ -635,7 +731,7 @@ class PyStage(BaseStage): | |
| 635 731 | 
             
                    gb: DictData = (
         | 
| 636 732 | 
             
                        globals()
         | 
| 637 733 | 
             
                        | params
         | 
| 638 | 
            -
                        | param2template(self.vars, params)
         | 
| 734 | 
            +
                        | param2template(self.vars, params, extras=self.extras)
         | 
| 639 735 | 
             
                        | {"result": result}
         | 
| 640 736 | 
             
                    )
         | 
| 641 737 |  | 
| @@ -648,7 +744,9 @@ class PyStage(BaseStage): | |
| 648 744 |  | 
| 649 745 | 
             
                    # WARNING: The exec build-in function is very dangerous. So, it
         | 
| 650 746 | 
             
                    #   should use the re module to validate exec-string before running.
         | 
| 651 | 
            -
                    exec( | 
| 747 | 
            +
                    exec(
         | 
| 748 | 
            +
                        param2template(dedent(self.run), params, extras=self.extras), gb, lc
         | 
| 749 | 
            +
                    )
         | 
| 652 750 |  | 
| 653 751 | 
             
                    return result.catch(
         | 
| 654 752 | 
             
                        status=Status.SUCCESS, context={"locals": lc, "globals": gb}
         | 
| @@ -716,11 +814,16 @@ class CallStage(BaseStage): | |
| 716 814 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 717 815 | 
             
                        )
         | 
| 718 816 |  | 
| 719 | 
            -
                    t_func: TagFunc = extract_call( | 
| 817 | 
            +
                    t_func: TagFunc = extract_call(
         | 
| 818 | 
            +
                        param2template(self.uses, params, extras=self.extras),
         | 
| 819 | 
            +
                        registries=self.extras.get("regis_call"),
         | 
| 820 | 
            +
                    )()
         | 
| 720 821 |  | 
| 721 822 | 
             
                    # VALIDATE: check input task caller parameters that exists before
         | 
| 722 823 | 
             
                    #   calling.
         | 
| 723 | 
            -
                    args: DictData = {"result": result} | param2template( | 
| 824 | 
            +
                    args: DictData = {"result": result} | param2template(
         | 
| 825 | 
            +
                        self.args, params, extras=self.extras
         | 
| 826 | 
            +
                    )
         | 
| 724 827 | 
             
                    ips = inspect.signature(t_func)
         | 
| 725 828 | 
             
                    necessary_params: list[str] = [
         | 
| 726 829 | 
             
                        k
         | 
| @@ -754,10 +857,12 @@ class CallStage(BaseStage): | |
| 754 857 | 
             
                    if inspect.iscoroutinefunction(t_func):  # pragma: no cov
         | 
| 755 858 | 
             
                        loop = asyncio.get_event_loop()
         | 
| 756 859 | 
             
                        rs: DictData = loop.run_until_complete(
         | 
| 757 | 
            -
                            t_func(**param2template(args, params))
         | 
| 860 | 
            +
                            t_func(**param2template(args, params, extras=self.extras))
         | 
| 758 861 | 
             
                        )
         | 
| 759 862 | 
             
                    else:
         | 
| 760 | 
            -
                        rs: DictData = t_func( | 
| 863 | 
            +
                        rs: DictData = t_func(
         | 
| 864 | 
            +
                            **param2template(args, params, extras=self.extras)
         | 
| 865 | 
            +
                        )
         | 
| 761 866 |  | 
| 762 867 | 
             
                    # VALIDATE:
         | 
| 763 868 | 
             
                    #   Check the result type from call function, it should be dict.
         | 
| @@ -819,15 +924,16 @@ class TriggerStage(BaseStage): | |
| 819 924 | 
             
                        )
         | 
| 820 925 |  | 
| 821 926 | 
             
                    # NOTE: Loading workflow object from trigger name.
         | 
| 822 | 
            -
                    _trigger: str = param2template(self.trigger, params= | 
| 927 | 
            +
                    _trigger: str = param2template(self.trigger, params, extras=self.extras)
         | 
| 823 928 |  | 
| 824 929 | 
             
                    # NOTE: Set running workflow ID from running stage ID to external
         | 
| 825 930 | 
             
                    #   params on Loader object.
         | 
| 826 | 
            -
                    workflow: Workflow = Workflow. | 
| 931 | 
            +
                    workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
         | 
| 827 932 | 
             
                    result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
         | 
| 828 933 | 
             
                    return workflow.execute(
         | 
| 829 | 
            -
                        params=param2template(self.params, params),
         | 
| 934 | 
            +
                        params=param2template(self.params, params, extras=self.extras),
         | 
| 830 935 | 
             
                        result=result,
         | 
| 936 | 
            +
                        event=event,
         | 
| 831 937 | 
             
                    )
         | 
| 832 938 |  | 
| 833 939 |  | 
| @@ -931,6 +1037,9 @@ class ParallelStage(BaseStage):  # pragma: no cov | |
| 931 1037 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 932 1038 | 
             
                        )
         | 
| 933 1039 |  | 
| 1040 | 
            +
                    result.trace.info(
         | 
| 1041 | 
            +
                        f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
         | 
| 1042 | 
            +
                    )
         | 
| 934 1043 | 
             
                    rs: DictData = {"parallel": {}}
         | 
| 935 1044 | 
             
                    status = Status.SUCCESS
         | 
| 936 1045 | 
             
                    with ThreadPoolExecutor(
         | 
| @@ -981,7 +1090,7 @@ class ForEachStage(BaseStage): | |
| 981 1090 | 
             
                    ... }
         | 
| 982 1091 | 
             
                """
         | 
| 983 1092 |  | 
| 984 | 
            -
                foreach: Union[list[str], list[int]] = Field(
         | 
| 1093 | 
            +
                foreach: Union[list[str], list[int], str] = Field(
         | 
| 985 1094 | 
             
                    description=(
         | 
| 986 1095 | 
             
                        "A items for passing to each stages via ${{ item }} template."
         | 
| 987 1096 | 
             
                    ),
         | 
| @@ -1023,10 +1132,21 @@ class ForEachStage(BaseStage): | |
| 1023 1132 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 1024 1133 | 
             
                        )
         | 
| 1025 1134 |  | 
| 1026 | 
            -
                     | 
| 1135 | 
            +
                    foreach: Union[list[str], list[int]] = (
         | 
| 1136 | 
            +
                        param2template(self.foreach, params, extras=self.extras)
         | 
| 1137 | 
            +
                        if isinstance(self.foreach, str)
         | 
| 1138 | 
            +
                        else self.foreach
         | 
| 1139 | 
            +
                    )
         | 
| 1140 | 
            +
                    if not isinstance(foreach, list):
         | 
| 1141 | 
            +
                        raise StageException(
         | 
| 1142 | 
            +
                            f"Foreach does not support foreach value: {foreach!r}"
         | 
| 1143 | 
            +
                        )
         | 
| 1144 | 
            +
             | 
| 1145 | 
            +
                    result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
         | 
| 1146 | 
            +
                    rs: DictData = {"items": foreach, "foreach": {}}
         | 
| 1027 1147 | 
             
                    status = Status.SUCCESS
         | 
| 1028 1148 | 
             
                    # TODO: Implement concurrent more than 1.
         | 
| 1029 | 
            -
                    for item in  | 
| 1149 | 
            +
                    for item in foreach:
         | 
| 1030 1150 | 
             
                        result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
         | 
| 1031 1151 | 
             
                        params["item"] = item
         | 
| 1032 1152 | 
             
                        context = {"stages": {}}
         | 
| @@ -1064,10 +1184,24 @@ class ForEachStage(BaseStage): | |
| 1064 1184 |  | 
| 1065 1185 | 
             
            # TODO: Not implement this stages yet
         | 
| 1066 1186 | 
             
            class UntilStage(BaseStage):  # pragma: no cov
         | 
| 1067 | 
            -
                """Until execution stage. | 
| 1187 | 
            +
                """Until execution stage.
         | 
| 1188 | 
            +
             | 
| 1189 | 
            +
                Data Validate:
         | 
| 1190 | 
            +
                    >>> stage = {
         | 
| 1191 | 
            +
                    ...     "name": "Until stage execution",
         | 
| 1192 | 
            +
                    ...     "item": 1,
         | 
| 1193 | 
            +
                    ...     "until": "${{ item }} > 3"
         | 
| 1194 | 
            +
                    ...     "stages": [
         | 
| 1195 | 
            +
                    ...         {
         | 
| 1196 | 
            +
                    ...             "name": "Start increase item value.",
         | 
| 1197 | 
            +
                    ...             "run": "item = ${{ item }}\\nitem += 1\\n"
         | 
| 1198 | 
            +
                    ...         },
         | 
| 1199 | 
            +
                    ...     ],
         | 
| 1200 | 
            +
                    ... }
         | 
| 1201 | 
            +
                """
         | 
| 1068 1202 |  | 
| 1069 | 
            -
                until: str = Field(description="A until condition.")
         | 
| 1070 1203 | 
             
                item: Union[str, int, bool] = Field(description="An initial value.")
         | 
| 1204 | 
            +
                until: str = Field(description="A until condition.")
         | 
| 1071 1205 | 
             
                stages: list[Stage] = Field(
         | 
| 1072 1206 | 
             
                    default_factory=list,
         | 
| 1073 1207 | 
             
                    description=(
         | 
| @@ -1090,8 +1224,14 @@ class UntilStage(BaseStage):  # pragma: no cov | |
| 1090 1224 |  | 
| 1091 1225 |  | 
| 1092 1226 | 
             
            # TODO: Not implement this stages yet
         | 
| 1093 | 
            -
            class  | 
| 1094 | 
            -
                 | 
| 1227 | 
            +
            class Match(BaseModel):
         | 
| 1228 | 
            +
                case: Union[str, int]
         | 
| 1229 | 
            +
                stage: Stage
         | 
| 1230 | 
            +
             | 
| 1231 | 
            +
             | 
| 1232 | 
            +
            # TODO: Not implement this stages yet
         | 
| 1233 | 
            +
            class CaseStage(BaseStage):  # pragma: no cov
         | 
| 1234 | 
            +
                """Case execution stage.
         | 
| 1095 1235 |  | 
| 1096 1236 | 
             
                Data Validate:
         | 
| 1097 1237 | 
             
                    >>> stage = {
         | 
| @@ -1125,7 +1265,7 @@ class IfStage(BaseStage):  # pragma: no cov | |
| 1125 1265 | 
             
                """
         | 
| 1126 1266 |  | 
| 1127 1267 | 
             
                case: str = Field(description="A case condition for routing.")
         | 
| 1128 | 
            -
                match: list[ | 
| 1268 | 
            +
                match: list[Match]
         | 
| 1129 1269 |  | 
| 1130 1270 | 
             
                def execute(
         | 
| 1131 1271 | 
             
                    self,
         | 
| @@ -1133,7 +1273,60 @@ class IfStage(BaseStage):  # pragma: no cov | |
| 1133 1273 | 
             
                    *,
         | 
| 1134 1274 | 
             
                    result: Result | None = None,
         | 
| 1135 1275 | 
             
                    event: Event | None = None,
         | 
| 1136 | 
            -
                ) -> Result: | 
| 1276 | 
            +
                ) -> Result:
         | 
| 1277 | 
            +
                    """Execute case-match condition that pass to the case field.
         | 
| 1278 | 
            +
             | 
| 1279 | 
            +
                    :param params: A parameter that want to pass before run any statement.
         | 
| 1280 | 
            +
                    :param result: (Result) A result object for keeping context and status
         | 
| 1281 | 
            +
                        data.
         | 
| 1282 | 
            +
                    :param event: (Event) An event manager that use to track parent execute
         | 
| 1283 | 
            +
                        was not force stopped.
         | 
| 1284 | 
            +
             | 
| 1285 | 
            +
                    :rtype: Result
         | 
| 1286 | 
            +
                    """
         | 
| 1287 | 
            +
                    if result is None:  # pragma: no cov
         | 
| 1288 | 
            +
                        result: Result = Result(
         | 
| 1289 | 
            +
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 1290 | 
            +
                        )
         | 
| 1291 | 
            +
                    status = Status.SUCCESS
         | 
| 1292 | 
            +
                    _case = param2template(self.case, params, extras=self.extras)
         | 
| 1293 | 
            +
                    _else = None
         | 
| 1294 | 
            +
                    context = {}
         | 
| 1295 | 
            +
                    for match in self.match:
         | 
| 1296 | 
            +
                        if (c := match.case) != "_":
         | 
| 1297 | 
            +
                            _condition = param2template(c, params, extras=self.extras)
         | 
| 1298 | 
            +
                        else:
         | 
| 1299 | 
            +
                            _else = match
         | 
| 1300 | 
            +
                            continue
         | 
| 1301 | 
            +
             | 
| 1302 | 
            +
                        if match == _condition:
         | 
| 1303 | 
            +
                            stage: Stage = match.stage
         | 
| 1304 | 
            +
                            try:
         | 
| 1305 | 
            +
                                stage.set_outputs(
         | 
| 1306 | 
            +
                                    stage.handler_execute(
         | 
| 1307 | 
            +
                                        params=params,
         | 
| 1308 | 
            +
                                        run_id=result.run_id,
         | 
| 1309 | 
            +
                                        parent_run_id=result.parent_run_id,
         | 
| 1310 | 
            +
                                    ).context,
         | 
| 1311 | 
            +
                                    to=context,
         | 
| 1312 | 
            +
                                )
         | 
| 1313 | 
            +
                            except StageException as err:  # pragma: no cov
         | 
| 1314 | 
            +
                                status = Status.FAILED
         | 
| 1315 | 
            +
                                result.trace.error(
         | 
| 1316 | 
            +
                                    f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
         | 
| 1317 | 
            +
                                    f"\n\t{err}"
         | 
| 1318 | 
            +
                                )
         | 
| 1319 | 
            +
                                context.update(
         | 
| 1320 | 
            +
                                    {
         | 
| 1321 | 
            +
                                        "errors": {
         | 
| 1322 | 
            +
                                            "class": err,
         | 
| 1323 | 
            +
                                            "name": err.__class__.__name__,
         | 
| 1324 | 
            +
                                            "message": f"{err.__class__.__name__}: {err}",
         | 
| 1325 | 
            +
                                        },
         | 
| 1326 | 
            +
                                    },
         | 
| 1327 | 
            +
                                )
         | 
| 1328 | 
            +
             | 
| 1329 | 
            +
                    return result.catch(status=status, context=context)
         | 
| 1137 1330 |  | 
| 1138 1331 |  | 
| 1139 1332 | 
             
            class RaiseStage(BaseStage):  # pragma: no cov
         | 
| @@ -1165,13 +1358,14 @@ class RaiseStage(BaseStage):  # pragma: no cov | |
| 1165 1358 | 
             
                        result: Result = Result(
         | 
| 1166 1359 | 
             
                            run_id=gen_id(self.name + (self.id or ""), unique=True)
         | 
| 1167 1360 | 
             
                        )
         | 
| 1168 | 
            -
             | 
| 1169 | 
            -
                    result.trace.error(f"[STAGE]: ... raise ( {self.message} )")
         | 
| 1361 | 
            +
                    result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
         | 
| 1170 1362 | 
             
                    raise StageException(self.message)
         | 
| 1171 1363 |  | 
| 1172 1364 |  | 
| 1173 1365 | 
             
            # TODO: Not implement this stages yet
         | 
| 1174 1366 | 
             
            class HookStage(BaseStage):  # pragma: no cov
         | 
| 1367 | 
            +
                """Hook stage execution."""
         | 
| 1368 | 
            +
             | 
| 1175 1369 | 
             
                hook: str
         | 
| 1176 1370 | 
             
                args: DictData
         | 
| 1177 1371 | 
             
                callback: str
         | 
| @@ -1187,7 +1381,20 @@ class HookStage(BaseStage):  # pragma: no cov | |
| 1187 1381 |  | 
| 1188 1382 | 
             
            # TODO: Not implement this stages yet
         | 
| 1189 1383 | 
             
            class DockerStage(BaseStage):  # pragma: no cov
         | 
| 1190 | 
            -
                """Docker container stage execution. | 
| 1384 | 
            +
                """Docker container stage execution.
         | 
| 1385 | 
            +
             | 
| 1386 | 
            +
                Data Validate:
         | 
| 1387 | 
            +
                    >>> stage = {
         | 
| 1388 | 
            +
                    ...     "name": "Docker stage execution",
         | 
| 1389 | 
            +
                    ...     "image": "image-name.pkg.com",
         | 
| 1390 | 
            +
                    ...     "env": {
         | 
| 1391 | 
            +
                    ...         "ENV": "dev",
         | 
| 1392 | 
            +
                    ...     },
         | 
| 1393 | 
            +
                    ...     "volume": {
         | 
| 1394 | 
            +
                    ...         "secrets": "/secrets",
         | 
| 1395 | 
            +
                    ...     },
         | 
| 1396 | 
            +
                    ... }
         | 
| 1397 | 
            +
                """
         | 
| 1191 1398 |  | 
| 1192 1399 | 
             
                image: str = Field(
         | 
| 1193 1400 | 
             
                    description="A Docker image url with tag that want to run.",
         | 
| @@ -1224,18 +1431,6 @@ class VirtualPyStage(PyStage):  # pragma: no cov | |
| 1224 1431 | 
             
                    return super().execute(params, result=result)
         | 
| 1225 1432 |  | 
| 1226 1433 |  | 
| 1227 | 
            -
            # TODO: Not implement this stages yet
         | 
| 1228 | 
            -
            class SensorStage(BaseStage):  # pragma: no cov
         | 
| 1229 | 
            -
             | 
| 1230 | 
            -
                def execute(
         | 
| 1231 | 
            -
                    self,
         | 
| 1232 | 
            -
                    params: DictData,
         | 
| 1233 | 
            -
                    *,
         | 
| 1234 | 
            -
                    result: Result | None = None,
         | 
| 1235 | 
            -
                    event: Event | None = None,
         | 
| 1236 | 
            -
                ) -> Result: ...
         | 
| 1237 | 
            -
             | 
| 1238 | 
            -
             | 
| 1239 1434 | 
             
            # NOTE:
         | 
| 1240 1435 | 
             
            #   An order of parsing stage model on the Job model with ``stages`` field.
         | 
| 1241 1436 | 
             
            #   From the current build-in stages, they do not have stage that have the same
         | 
| @@ -1243,7 +1438,6 @@ class SensorStage(BaseStage):  # pragma: no cov | |
| 1243 1438 | 
             
            #
         | 
| 1244 1439 | 
             
            Stage = Annotated[
         | 
| 1245 1440 | 
             
                Union[
         | 
| 1246 | 
            -
                    EmptyStage,
         | 
| 1247 1441 | 
             
                    BashStage,
         | 
| 1248 1442 | 
             
                    CallStage,
         | 
| 1249 1443 | 
             
                    TriggerStage,
         | 
| @@ -1251,6 +1445,7 @@ Stage = Annotated[ | |
| 1251 1445 | 
             
                    ParallelStage,
         | 
| 1252 1446 | 
             
                    PyStage,
         | 
| 1253 1447 | 
             
                    RaiseStage,
         | 
| 1448 | 
            +
                    EmptyStage,
         | 
| 1254 1449 | 
             
                ],
         | 
| 1255 1450 | 
             
                Field(union_mode="smart"),
         | 
| 1256 1451 | 
             
            ]
         | 
    
        ddeutil/workflow/utils.py
    CHANGED