ddeutil-workflow 0.0.21__tar.gz → 0.0.23__tar.gz
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-0.0.21/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.23}/PKG-INFO +2 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/README.md +1 -0
- ddeutil_workflow-0.0.23/src/ddeutil/workflow/__about__.py +1 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/__init__.py +9 -8
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/api.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/conf.py +4 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/job.py +28 -18
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/on.py +6 -3
- ddeutil_workflow-0.0.23/src/ddeutil/workflow/params.py +176 -0
- ddeutil_workflow-0.0.23/src/ddeutil/workflow/result.py +102 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/route.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/scheduler.py +38 -26
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/stage.py +18 -10
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/utils.py +22 -243
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/workflow.py +230 -125
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23/src/ddeutil_workflow.egg-info}/PKG-INFO +2 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil_workflow.egg-info/SOURCES.txt +6 -3
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_job.py +5 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_job_exec_py.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_job_exec_strategy.py +1 -1
- ddeutil_workflow-0.0.21/tests/test_utils_params.py → ddeutil_workflow-0.0.23/tests/test_params.py +5 -1
- ddeutil_workflow-0.0.21/tests/test_utils_result.py → ddeutil_workflow-0.0.23/tests/test_result.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_scheduler.py +3 -3
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_scheduler_tasks.py +0 -19
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_stage.py +37 -15
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_stage_exec_bash.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_stage_exec_hook.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_stage_exec_py.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_stage_exec_trigger.py +1 -1
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_utils.py +16 -0
- ddeutil_workflow-0.0.23/tests/test_workflow.py +254 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_workflow_exec.py +2 -2
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_workflow_exec_needs.py +1 -1
- ddeutil_workflow-0.0.23/tests/test_workflow_job_exec.py +62 -0
- ddeutil_workflow-0.0.23/tests/test_workflow_poke.py +151 -0
- ddeutil_workflow-0.0.23/tests/test_workflow_release.py +165 -0
- ddeutil_workflow-0.0.23/tests/test_workflow_schedule.py +58 -0
- ddeutil_workflow-0.0.23/tests/test_workflow_task_data.py +82 -0
- ddeutil_workflow-0.0.21/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.21/tests/test_params.py +0 -13
- ddeutil_workflow-0.0.21/tests/test_workflow.py +0 -188
- ddeutil_workflow-0.0.21/tests/test_workflow_job_exec.py +0 -28
- ddeutil_workflow-0.0.21/tests/test_workflow_poke.py +0 -50
- ddeutil_workflow-0.0.21/tests/test_workflow_release.py +0 -44
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/LICENSE +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/pyproject.toml +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/__cron.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/cli.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/exceptions.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil/workflow/repeat.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_conf_log.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_job_strategy.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_on.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_utils_filter.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_utils_tag.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_utils_template.py +0 -0
- {ddeutil_workflow-0.0.21 → ddeutil_workflow-0.0.23}/tests/test_workflow_exec_hook.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.23
|
4
4
|
Summary: Lightweight workflow orchestration with less dependencies
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -196,6 +196,7 @@ and do not raise any error to you.
|
|
196
196
|
| `WORKFLOW_CORE_MAX_NUM_POKING` | Core | 4 | . | |
|
197
197
|
| `WORKFLOW_CORE_MAX_JOB_PARALLEL` | Core | 2 | The maximum job number that able to run parallel in workflow executor. | |
|
198
198
|
| `WORKFLOW_CORE_MAX_JOB_EXEC_TIMEOUT` | Core | 600 | | |
|
199
|
+
| `WORKFLOW_CORE_MAX_ON_PER_WORKFLOW` | Core | 5 | | |
|
199
200
|
| `WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE` | Core | true | A flog that enable generating ID with `md5` algorithm. | |
|
200
201
|
| `WORKFLOW_LOG_DEBUG_MODE` | Log | true | A flag that enable logging with debug level mode. | |
|
201
202
|
| `WORKFLOW_LOG_ENABLE_WRITE` | Log | true | A flag that enable logging object saving log to its destination. | |
|
@@ -163,6 +163,7 @@ and do not raise any error to you.
|
|
163
163
|
| `WORKFLOW_CORE_MAX_NUM_POKING` | Core | 4 | . | |
|
164
164
|
| `WORKFLOW_CORE_MAX_JOB_PARALLEL` | Core | 2 | The maximum job number that able to run parallel in workflow executor. | |
|
165
165
|
| `WORKFLOW_CORE_MAX_JOB_EXEC_TIMEOUT` | Core | 600 | | |
|
166
|
+
| `WORKFLOW_CORE_MAX_ON_PER_WORKFLOW` | Core | 5 | | |
|
166
167
|
| `WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE` | Core | true | A flog that enable generating ID with `md5` algorithm. | |
|
167
168
|
| `WORKFLOW_LOG_DEBUG_MODE` | Log | true | A flag that enable logging with debug level mode. | |
|
168
169
|
| `WORKFLOW_LOG_ENABLE_WRITE` | Log | true | A flag that enable logging object saving log to its destination. | |
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.23"
|
@@ -24,9 +24,17 @@ from .on import (
|
|
24
24
|
YearOn,
|
25
25
|
interval2crontab,
|
26
26
|
)
|
27
|
+
from .params import (
|
28
|
+
ChoiceParam,
|
29
|
+
DatetimeParam,
|
30
|
+
IntParam,
|
31
|
+
Param,
|
32
|
+
StrParam,
|
33
|
+
)
|
34
|
+
from .result import Result
|
27
35
|
from .scheduler import (
|
28
36
|
Schedule,
|
29
|
-
|
37
|
+
WorkflowSchedule,
|
30
38
|
)
|
31
39
|
from .stage import (
|
32
40
|
BashStage,
|
@@ -39,16 +47,9 @@ from .stage import (
|
|
39
47
|
)
|
40
48
|
from .utils import (
|
41
49
|
FILTERS,
|
42
|
-
ChoiceParam,
|
43
|
-
DatetimeParam,
|
44
|
-
DefaultParam,
|
45
50
|
FilterFunc,
|
46
51
|
FilterRegistry,
|
47
|
-
IntParam,
|
48
|
-
Param,
|
49
|
-
Result,
|
50
52
|
ReturnTagFunc,
|
51
|
-
StrParam,
|
52
53
|
TagFunc,
|
53
54
|
batch,
|
54
55
|
cross_product,
|
@@ -23,7 +23,7 @@ from pydantic import BaseModel
|
|
23
23
|
from .__about__ import __version__
|
24
24
|
from .conf import config, get_logger
|
25
25
|
from .repeat import repeat_at, repeat_every
|
26
|
-
from .
|
26
|
+
from .workflow import WorkflowTaskData
|
27
27
|
|
28
28
|
load_dotenv()
|
29
29
|
logger = get_logger("ddeutil.workflow")
|
@@ -106,6 +106,9 @@ class Config:
|
|
106
106
|
max_poking_pool_worker: int = int(
|
107
107
|
os.getenv("WORKFLOW_CORE_MAX_NUM_POKING", "4")
|
108
108
|
)
|
109
|
+
max_on_per_workflow: int = int(
|
110
|
+
env("WORKFLOW_CORE_MAX_ON_PER_WORKFLOW", "5")
|
111
|
+
)
|
109
112
|
|
110
113
|
# NOTE: Schedule App
|
111
114
|
max_schedule_process: int = int(env("WORKFLOW_APP_MAX_PROCESS", "2"))
|
@@ -462,6 +465,7 @@ class FileLog(BaseLog):
|
|
462
465
|
|
463
466
|
:param excluded: An excluded list of key name that want to pass in the
|
464
467
|
model_dump method.
|
468
|
+
|
465
469
|
:rtype: Self
|
466
470
|
"""
|
467
471
|
# NOTE: Check environ variable was set for real writing.
|
@@ -22,7 +22,7 @@ from enum import Enum
|
|
22
22
|
from functools import lru_cache
|
23
23
|
from textwrap import dedent
|
24
24
|
from threading import Event
|
25
|
-
from typing import Optional, Union
|
25
|
+
from typing import Any, Optional, Union
|
26
26
|
|
27
27
|
from ddeutil.core import freeze_args
|
28
28
|
from pydantic import BaseModel, Field
|
@@ -36,10 +36,11 @@ from .exceptions import (
|
|
36
36
|
StageException,
|
37
37
|
UtilException,
|
38
38
|
)
|
39
|
+
from .result import Result
|
39
40
|
from .stage import Stage
|
40
41
|
from .utils import (
|
41
|
-
Result,
|
42
42
|
cross_product,
|
43
|
+
cut_id,
|
43
44
|
dash2underscore,
|
44
45
|
filter_func,
|
45
46
|
gen_id,
|
@@ -312,7 +313,7 @@ class Job(BaseModel):
|
|
312
313
|
# VALIDATE: Validate stage id should not duplicate.
|
313
314
|
rs: list[str] = []
|
314
315
|
for stage in value:
|
315
|
-
name: str = stage.
|
316
|
+
name: str = stage.iden
|
316
317
|
if name in rs:
|
317
318
|
raise ValueError(
|
318
319
|
"Stage name in jobs object should not be duplicate."
|
@@ -346,6 +347,13 @@ class Job(BaseModel):
|
|
346
347
|
return stage
|
347
348
|
raise ValueError(f"Stage ID {stage_id} does not exists")
|
348
349
|
|
350
|
+
def check_needs(self, jobs: dict[str, Any]) -> bool:
|
351
|
+
"""Return True if job's need exists in an input list of job's ID.
|
352
|
+
|
353
|
+
:rtype: bool
|
354
|
+
"""
|
355
|
+
return all(need in jobs for need in self.needs)
|
356
|
+
|
349
357
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
350
358
|
"""Set an outputs from execution process to the receive context. The
|
351
359
|
result from execution will pass to value of ``strategies`` key.
|
@@ -427,6 +435,7 @@ class Job(BaseModel):
|
|
427
435
|
"""
|
428
436
|
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
429
437
|
strategy_id: str = gen_id(strategy)
|
438
|
+
rs: Result = Result(run_id=run_id)
|
430
439
|
|
431
440
|
# PARAGRAPH:
|
432
441
|
#
|
@@ -447,14 +456,18 @@ class Job(BaseModel):
|
|
447
456
|
for stage in self.stages:
|
448
457
|
|
449
458
|
if stage.is_skipped(params=context):
|
450
|
-
logger.info(
|
459
|
+
logger.info(
|
460
|
+
f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
|
461
|
+
)
|
451
462
|
continue
|
452
463
|
|
453
|
-
logger.info(
|
464
|
+
logger.info(
|
465
|
+
f"({cut_id(run_id)}) [JOB]: Execute stage: {stage.iden!r}"
|
466
|
+
)
|
454
467
|
|
455
468
|
# NOTE: Logging a matrix that pass on this stage execution.
|
456
469
|
if strategy:
|
457
|
-
logger.info(f"({run_id}) [JOB]: ... Matrix: {strategy}")
|
470
|
+
logger.info(f"({cut_id(run_id)}) [JOB]: ... Matrix: {strategy}")
|
458
471
|
|
459
472
|
# NOTE: Force stop this execution if event was set from main
|
460
473
|
# execution.
|
@@ -463,7 +476,7 @@ class Job(BaseModel):
|
|
463
476
|
"Job strategy was canceled from event that had set before "
|
464
477
|
"strategy execution."
|
465
478
|
)
|
466
|
-
return
|
479
|
+
return rs.catch(
|
467
480
|
status=1,
|
468
481
|
context={
|
469
482
|
strategy_id: {
|
@@ -478,7 +491,6 @@ class Job(BaseModel):
|
|
478
491
|
"error_message": error_msg,
|
479
492
|
},
|
480
493
|
},
|
481
|
-
run_id=run_id,
|
482
494
|
)
|
483
495
|
|
484
496
|
# PARAGRAPH:
|
@@ -506,14 +518,14 @@ class Job(BaseModel):
|
|
506
518
|
)
|
507
519
|
except (StageException, UtilException) as err:
|
508
520
|
logger.error(
|
509
|
-
f"({run_id}) [JOB]: {err.__class__.__name__}: {err}"
|
521
|
+
f"({cut_id(run_id)}) [JOB]: {err.__class__.__name__}: {err}"
|
510
522
|
)
|
511
523
|
if config.job_raise_error:
|
512
524
|
raise JobException(
|
513
525
|
f"Get stage execution error: {err.__class__.__name__}: "
|
514
526
|
f"{err}"
|
515
527
|
) from None
|
516
|
-
return
|
528
|
+
return rs.catch(
|
517
529
|
status=1,
|
518
530
|
context={
|
519
531
|
strategy_id: {
|
@@ -523,13 +535,12 @@ class Job(BaseModel):
|
|
523
535
|
"error_message": f"{err.__class__.__name__}: {err}",
|
524
536
|
},
|
525
537
|
},
|
526
|
-
run_id=run_id,
|
527
538
|
)
|
528
539
|
|
529
540
|
# NOTE: Remove the current stage object for saving memory.
|
530
541
|
del stage
|
531
542
|
|
532
|
-
return
|
543
|
+
return rs.catch(
|
533
544
|
status=0,
|
534
545
|
context={
|
535
546
|
strategy_id: {
|
@@ -537,7 +548,6 @@ class Job(BaseModel):
|
|
537
548
|
"stages": filter_func(context.pop("stages", {})),
|
538
549
|
},
|
539
550
|
},
|
540
|
-
run_id=run_id,
|
541
551
|
)
|
542
552
|
|
543
553
|
def execute(self, params: DictData, run_id: str | None = None) -> Result:
|
@@ -619,7 +629,7 @@ class Job(BaseModel):
|
|
619
629
|
|
620
630
|
:rtype: Result
|
621
631
|
"""
|
622
|
-
rs_final: Result = Result()
|
632
|
+
rs_final: Result = Result(run_id=run_id)
|
623
633
|
context: DictData = {}
|
624
634
|
status: int = 0
|
625
635
|
|
@@ -631,7 +641,7 @@ class Job(BaseModel):
|
|
631
641
|
nd: str = (
|
632
642
|
f", the strategies do not run is {not_done}" if not_done else ""
|
633
643
|
)
|
634
|
-
logger.debug(f"({run_id}) [JOB]: Strategy is set Fail Fast{nd}")
|
644
|
+
logger.debug(f"({cut_id(run_id)}) [JOB]: Strategy is set Fail Fast{nd}")
|
635
645
|
|
636
646
|
# NOTE:
|
637
647
|
# Stop all running tasks with setting the event manager and cancel
|
@@ -649,7 +659,7 @@ class Job(BaseModel):
|
|
649
659
|
if err := future.exception():
|
650
660
|
status: int = 1
|
651
661
|
logger.error(
|
652
|
-
f"({run_id}) [JOB]: Fail-fast catching:\n\t"
|
662
|
+
f"({cut_id(run_id)}) [JOB]: Fail-fast catching:\n\t"
|
653
663
|
f"{future.exception()}"
|
654
664
|
)
|
655
665
|
context.update(
|
@@ -680,7 +690,7 @@ class Job(BaseModel):
|
|
680
690
|
|
681
691
|
:rtype: Result
|
682
692
|
"""
|
683
|
-
rs_final: Result = Result()
|
693
|
+
rs_final: Result = Result(run_id=run_id)
|
684
694
|
context: DictData = {}
|
685
695
|
status: int = 0
|
686
696
|
|
@@ -690,7 +700,7 @@ class Job(BaseModel):
|
|
690
700
|
except JobException as err:
|
691
701
|
status = 1
|
692
702
|
logger.error(
|
693
|
-
f"({run_id}) [JOB]: All-completed catching:\n\t"
|
703
|
+
f"({cut_id(run_id)}) [JOB]: All-completed catching:\n\t"
|
694
704
|
f"{err.__class__.__name__}:\n\t{err}"
|
695
705
|
)
|
696
706
|
context.update(
|
@@ -61,7 +61,7 @@ def interval2crontab(
|
|
61
61
|
|
62
62
|
|
63
63
|
class On(BaseModel):
|
64
|
-
"""On
|
64
|
+
"""On Pydantic model (Warped crontab object by model).
|
65
65
|
|
66
66
|
See Also:
|
67
67
|
* ``generate()`` is the main usecase of this schedule object.
|
@@ -189,13 +189,16 @@ class On(BaseModel):
|
|
189
189
|
date that given from input.
|
190
190
|
"""
|
191
191
|
runner: CronRunner = self.generate(start=start)
|
192
|
+
|
193
|
+
# NOTE: ship the next date of runner object that create from start.
|
192
194
|
_ = runner.next
|
195
|
+
|
193
196
|
return runner
|
194
197
|
|
195
198
|
|
196
199
|
class YearOn(On):
|
197
|
-
"""
|
198
|
-
data schedule tools like AWS Glue.
|
200
|
+
"""On with enhance Year Pydantic model for limit year matrix that use by
|
201
|
+
some data schedule tools like AWS Glue.
|
199
202
|
"""
|
200
203
|
|
201
204
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from abc import ABC, abstractmethod
|
10
|
+
from datetime import date, datetime
|
11
|
+
from typing import Any, Literal, Optional, Union
|
12
|
+
|
13
|
+
from pydantic import BaseModel, Field
|
14
|
+
|
15
|
+
from .__types import TupleStr
|
16
|
+
from .exceptions import ParamValueException
|
17
|
+
from .utils import get_dt_now
|
18
|
+
|
19
|
+
logger = logging.getLogger("ddeutil.workflow")
|
20
|
+
|
21
|
+
__all__: TupleStr = (
|
22
|
+
"ChoiceParam",
|
23
|
+
"DatetimeParam",
|
24
|
+
"IntParam",
|
25
|
+
"Param",
|
26
|
+
"StrParam",
|
27
|
+
)
|
28
|
+
|
29
|
+
|
30
|
+
class BaseParam(BaseModel, ABC):
|
31
|
+
"""Base Parameter that use to make any Params Model. The type will dynamic
|
32
|
+
with the type field that made from literal string."""
|
33
|
+
|
34
|
+
desc: Optional[str] = Field(
|
35
|
+
default=None, description="A description of parameter providing."
|
36
|
+
)
|
37
|
+
required: bool = Field(
|
38
|
+
default=True,
|
39
|
+
description="A require flag that force to pass this parameter value.",
|
40
|
+
)
|
41
|
+
type: str = Field(description="A type of parameter.")
|
42
|
+
|
43
|
+
@abstractmethod
|
44
|
+
def receive(self, value: Optional[Any] = None) -> Any:
|
45
|
+
raise NotImplementedError(
|
46
|
+
"Receive value and validate typing before return valid value."
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
class DefaultParam(BaseParam):
|
51
|
+
"""Default Parameter that will check default if it required. This model do
|
52
|
+
not implement the receive method.
|
53
|
+
"""
|
54
|
+
|
55
|
+
required: bool = Field(
|
56
|
+
default=False,
|
57
|
+
description="A require flag for the default-able parameter value.",
|
58
|
+
)
|
59
|
+
default: Optional[str] = Field(
|
60
|
+
default=None,
|
61
|
+
description="A default value if parameter does not pass.",
|
62
|
+
)
|
63
|
+
|
64
|
+
@abstractmethod
|
65
|
+
def receive(self, value: Optional[Any] = None) -> Any:
|
66
|
+
raise NotImplementedError(
|
67
|
+
"Receive value and validate typing before return valid value."
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
class DatetimeParam(DefaultParam):
|
72
|
+
"""Datetime parameter."""
|
73
|
+
|
74
|
+
type: Literal["datetime"] = "datetime"
|
75
|
+
default: datetime = Field(default_factory=get_dt_now)
|
76
|
+
|
77
|
+
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
78
|
+
"""Receive value that match with datetime. If a input value pass with
|
79
|
+
None, it will use default value instead.
|
80
|
+
|
81
|
+
:param value: A value that want to validate with datetime parameter
|
82
|
+
type.
|
83
|
+
:rtype: datetime
|
84
|
+
"""
|
85
|
+
if value is None:
|
86
|
+
return self.default
|
87
|
+
|
88
|
+
if isinstance(value, datetime):
|
89
|
+
return value
|
90
|
+
elif isinstance(value, date):
|
91
|
+
return datetime(value.year, value.month, value.day)
|
92
|
+
elif not isinstance(value, str):
|
93
|
+
raise ParamValueException(
|
94
|
+
f"Value that want to convert to datetime does not support for "
|
95
|
+
f"type: {type(value)}"
|
96
|
+
)
|
97
|
+
try:
|
98
|
+
return datetime.fromisoformat(value)
|
99
|
+
except ValueError:
|
100
|
+
raise ParamValueException(
|
101
|
+
f"Invalid isoformat string: {value!r}"
|
102
|
+
) from None
|
103
|
+
|
104
|
+
|
105
|
+
class StrParam(DefaultParam):
|
106
|
+
"""String parameter."""
|
107
|
+
|
108
|
+
type: Literal["str"] = "str"
|
109
|
+
|
110
|
+
def receive(self, value: str | None = None) -> str | None:
|
111
|
+
"""Receive value that match with str.
|
112
|
+
|
113
|
+
:param value: A value that want to validate with string parameter type.
|
114
|
+
:rtype: str | None
|
115
|
+
"""
|
116
|
+
if value is None:
|
117
|
+
return self.default
|
118
|
+
return str(value)
|
119
|
+
|
120
|
+
|
121
|
+
class IntParam(DefaultParam):
|
122
|
+
"""Integer parameter."""
|
123
|
+
|
124
|
+
type: Literal["int"] = "int"
|
125
|
+
default: Optional[int] = Field(
|
126
|
+
default=None,
|
127
|
+
description="A default value if parameter does not pass.",
|
128
|
+
)
|
129
|
+
|
130
|
+
def receive(self, value: int | None = None) -> int | None:
|
131
|
+
"""Receive value that match with int.
|
132
|
+
|
133
|
+
:param value: A value that want to validate with integer parameter type.
|
134
|
+
:rtype: int | None
|
135
|
+
"""
|
136
|
+
if value is None:
|
137
|
+
return self.default
|
138
|
+
if not isinstance(value, int):
|
139
|
+
try:
|
140
|
+
return int(str(value))
|
141
|
+
except ValueError as err:
|
142
|
+
raise ParamValueException(
|
143
|
+
f"Value can not convert to int, {value}, with base 10"
|
144
|
+
) from err
|
145
|
+
return value
|
146
|
+
|
147
|
+
|
148
|
+
class ChoiceParam(BaseParam):
|
149
|
+
"""Choice parameter."""
|
150
|
+
|
151
|
+
type: Literal["choice"] = "choice"
|
152
|
+
options: list[str] = Field(description="A list of choice parameters.")
|
153
|
+
|
154
|
+
def receive(self, value: str | None = None) -> str:
|
155
|
+
"""Receive value that match with options.
|
156
|
+
|
157
|
+
:param value: A value that want to select from the options field.
|
158
|
+
:rtype: str
|
159
|
+
"""
|
160
|
+
# NOTE:
|
161
|
+
# Return the first value in options if does not pass any input value
|
162
|
+
if value is None:
|
163
|
+
return self.options[0]
|
164
|
+
if value not in self.options:
|
165
|
+
raise ParamValueException(
|
166
|
+
f"{value!r} does not match any value in choice options."
|
167
|
+
)
|
168
|
+
return value
|
169
|
+
|
170
|
+
|
171
|
+
Param = Union[
|
172
|
+
ChoiceParam,
|
173
|
+
DatetimeParam,
|
174
|
+
IntParam,
|
175
|
+
StrParam,
|
176
|
+
]
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
from dataclasses import field
|
9
|
+
from typing import Optional
|
10
|
+
|
11
|
+
from pydantic.dataclasses import dataclass
|
12
|
+
from pydantic.functional_validators import model_validator
|
13
|
+
from typing_extensions import Self
|
14
|
+
|
15
|
+
from .__types import DictData, TupleStr
|
16
|
+
from .utils import gen_id
|
17
|
+
|
18
|
+
__all__: TupleStr = ("Result",)
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class Result:
|
23
|
+
"""Result Pydantic Model for passing and receiving data context from any
|
24
|
+
module execution process like stage execution, job execution, or workflow
|
25
|
+
execution.
|
26
|
+
|
27
|
+
For comparison property, this result will use ``status``, ``context``,
|
28
|
+
and ``_run_id`` fields to comparing with other result instance.
|
29
|
+
"""
|
30
|
+
|
31
|
+
status: int = field(default=2)
|
32
|
+
context: DictData = field(default_factory=dict)
|
33
|
+
run_id: Optional[str] = field(default=None)
|
34
|
+
|
35
|
+
# NOTE: Ignore this field to compare another result model with __eq__.
|
36
|
+
parent_run_id: Optional[str] = field(default=None, compare=False)
|
37
|
+
|
38
|
+
@model_validator(mode="after")
|
39
|
+
def __prepare_run_id(self) -> Self:
|
40
|
+
"""Prepare running ID which use default ID if it initialize at the first
|
41
|
+
time
|
42
|
+
|
43
|
+
:rtype: Self
|
44
|
+
"""
|
45
|
+
self._run_id = gen_id("manual", unique=True)
|
46
|
+
return self
|
47
|
+
|
48
|
+
def set_run_id(self, running_id: str) -> Self:
|
49
|
+
"""Set a running ID.
|
50
|
+
|
51
|
+
:param running_id: A running ID that want to update on this model.
|
52
|
+
:rtype: Self
|
53
|
+
"""
|
54
|
+
self.run_id = running_id
|
55
|
+
return self
|
56
|
+
|
57
|
+
def set_parent_run_id(self, running_id: str) -> Self:
|
58
|
+
"""Set a parent running ID.
|
59
|
+
|
60
|
+
:param running_id: A running ID that want to update on this model.
|
61
|
+
:rtype: Self
|
62
|
+
"""
|
63
|
+
self.parent_run_id: str = running_id
|
64
|
+
return self
|
65
|
+
|
66
|
+
def catch(self, status: int, context: DictData) -> Self:
|
67
|
+
"""Catch the status and context to current data."""
|
68
|
+
self.__dict__["status"] = status
|
69
|
+
self.__dict__["context"].update(context)
|
70
|
+
return self
|
71
|
+
|
72
|
+
def receive(self, result: Result) -> Self:
|
73
|
+
"""Receive context from another result object.
|
74
|
+
|
75
|
+
:rtype: Self
|
76
|
+
"""
|
77
|
+
self.__dict__["status"] = result.status
|
78
|
+
self.__dict__["context"].update(result.context)
|
79
|
+
|
80
|
+
# NOTE: Update running ID from an incoming result.
|
81
|
+
self.parent_run_id = result.parent_run_id
|
82
|
+
self.run_id = result.run_id
|
83
|
+
return self
|
84
|
+
|
85
|
+
def receive_jobs(self, result: Result) -> Self:
|
86
|
+
"""Receive context from another result object that use on the workflow
|
87
|
+
execution which create a ``jobs`` keys on the context if it do not
|
88
|
+
exist.
|
89
|
+
|
90
|
+
:rtype: Self
|
91
|
+
"""
|
92
|
+
self.__dict__["status"] = result.status
|
93
|
+
|
94
|
+
# NOTE: Check the context has jobs key.
|
95
|
+
if "jobs" not in self.__dict__["context"]:
|
96
|
+
self.__dict__["context"]["jobs"] = {}
|
97
|
+
self.__dict__["context"]["jobs"].update(result.context)
|
98
|
+
|
99
|
+
# NOTE: Update running ID from an incoming result.
|
100
|
+
self.parent_run_id: str = result.parent_run_id
|
101
|
+
self.run_id: str = result.run_id
|
102
|
+
return self
|
@@ -17,8 +17,8 @@ from pydantic import BaseModel
|
|
17
17
|
from . import Workflow
|
18
18
|
from .__types import DictData
|
19
19
|
from .conf import Loader, config, get_logger
|
20
|
+
from .result import Result
|
20
21
|
from .scheduler import Schedule
|
21
|
-
from .utils import Result
|
22
22
|
|
23
23
|
logger = get_logger("ddeutil.workflow")
|
24
24
|
workflow = APIRouter(
|