ddeutil-workflow 0.0.21__py3-none-any.whl → 0.0.23__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.21"
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
- ScheduleWorkflow,
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,
ddeutil/workflow/api.py CHANGED
@@ -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 .scheduler import WorkflowTaskData
26
+ from .workflow import WorkflowTaskData
27
27
 
28
28
  load_dotenv()
29
29
  logger = get_logger("ddeutil.workflow")
ddeutil/workflow/conf.py CHANGED
@@ -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.
ddeutil/workflow/job.py CHANGED
@@ -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.id or stage.name
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(f"({run_id}) [JOB]: Skip stage: {stage.iden!r}")
459
+ logger.info(
460
+ f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
461
+ )
451
462
  continue
452
463
 
453
- logger.info(f"({run_id}) [JOB]: Execute stage: {stage.iden!r}")
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 Result(
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 Result(
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 Result(
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(
ddeutil/workflow/on.py CHANGED
@@ -61,7 +61,7 @@ def interval2crontab(
61
61
 
62
62
 
63
63
  class On(BaseModel):
64
- """On Model (Schedule)
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
- """Implement On Year Schedule Model for limit year matrix that use by some
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
ddeutil/workflow/route.py CHANGED
@@ -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(