ddeutil-workflow 0.0.59__py3-none-any.whl → 0.0.60__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.59"
1
+ __version__: str = "0.0.60"
@@ -20,6 +20,7 @@ from typing import Any, Optional, TypedDict, Union
20
20
 
21
21
  from typing_extensions import Self
22
22
 
23
+ StrOrNone = Optional[str]
23
24
  StrOrInt = Union[str, int]
24
25
  TupleStr = tuple[str, ...]
25
26
  DictData = dict[str, Any]
@@ -42,7 +43,7 @@ class CallerRe:
42
43
 
43
44
  full: str
44
45
  caller: str
45
- caller_prefix: Optional[str]
46
+ caller_prefix: StrOrNone
46
47
  caller_last: str
47
48
  post_filters: str
48
49
 
@@ -50,6 +51,9 @@ class CallerRe:
50
51
  def from_regex(cls, match: Match[str]) -> Self:
51
52
  """Class construct from matching result.
52
53
 
54
+ :param match: A match string object for contract this Caller regex data
55
+ class.
56
+
53
57
  :rtype: Self
54
58
  """
55
59
  return cls(full=match.group(0), **match.groupdict())
@@ -121,10 +125,13 @@ class Re:
121
125
  )
122
126
 
123
127
  @classmethod
124
- def finditer_caller(cls, value) -> Iterator[CallerRe]:
128
+ def finditer_caller(cls, value: str) -> Iterator[CallerRe]:
125
129
  """Generate CallerRe object that create from matching object that
126
130
  extract with re.finditer function.
127
131
 
132
+ :param value: (str) A string value that want to finditer with the caller
133
+ regular expression.
134
+
128
135
  :rtype: Iterator[CallerRe]
129
136
  """
130
137
  for found in cls.RE_CALLER.finditer(value):
ddeutil/workflow/event.py CHANGED
@@ -3,8 +3,9 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Event module that store all event object. Now, it has only `Crontab` and
7
- `CrontabYear` model these are schedule with crontab event.
6
+ """Event module include all event object for trigger the Workflow to release.
7
+ Now, it has only `Crontab` and `CrontabYear` event models on this module because
8
+ I think it is the core event for workflow orchestration.
8
9
  """
9
10
  from __future__ import annotations
10
11
 
@@ -86,7 +87,7 @@ class Crontab(BaseModel):
86
87
  CronJob,
87
88
  Field(
88
89
  description=(
89
- "A Cronjob object that use for validate and generate datetime.",
90
+ "A Cronjob object that use for validate and generate datetime."
90
91
  ),
91
92
  ),
92
93
  ]
@@ -117,7 +118,6 @@ class Crontab(BaseModel):
117
118
  passing["cronjob"] = interval2crontab(
118
119
  **{v: value[v] for v in value if v in ("interval", "day", "time")}
119
120
  )
120
- print(passing)
121
121
  return cls(extras=extras | passing.pop("extras", {}), **passing)
122
122
 
123
123
  @classmethod
@@ -170,9 +170,10 @@ class Crontab(BaseModel):
170
170
 
171
171
  @model_validator(mode="before")
172
172
  def __prepare_values(cls, data: Any) -> Any:
173
- """Extract tz key from value and change name to timezone key.
173
+ """Extract a `tz` key from data and change the key name from `tz` to
174
+ `timezone`.
174
175
 
175
- :param data: (DictData) A data that want to pass for create an Crontab
176
+ :param data: (DictData) A data that want to pass for create a Crontab
176
177
  model.
177
178
 
178
179
  :rtype: DictData
@@ -198,7 +199,7 @@ class Crontab(BaseModel):
198
199
  "cronjob", mode="before", json_schema_input_type=Union[CronJob, str]
199
200
  )
200
201
  def __prepare_cronjob(
201
- cls, value: str | CronJob, info: ValidationInfo
202
+ cls, value: Union[str, CronJob], info: ValidationInfo
202
203
  ) -> CronJob:
203
204
  """Prepare crontab value that able to receive with string type.
204
205
  This step will get options kwargs from extras field and pass to the
@@ -234,7 +235,7 @@ class Crontab(BaseModel):
234
235
  """
235
236
  return str(value)
236
237
 
237
- def generate(self, start: str | datetime) -> CronRunner:
238
+ def generate(self, start: Union[str, datetime]) -> CronRunner:
238
239
  """Return CronRunner object from an initial datetime.
239
240
 
240
241
  :param start: (str | datetime) A string or datetime for generate the
@@ -248,7 +249,7 @@ class Crontab(BaseModel):
248
249
  raise TypeError("start value should be str or datetime type.")
249
250
  return self.cronjob.schedule(date=start, tz=self.tz)
250
251
 
251
- def next(self, start: str | datetime) -> CronRunner:
252
+ def next(self, start: Union[str, datetime]) -> CronRunner:
252
253
  """Return a next datetime from Cron runner object that start with any
253
254
  date that given from input.
254
255
 
@@ -277,16 +278,18 @@ class CrontabYear(Crontab):
277
278
  CronJobYear,
278
279
  Field(
279
280
  description=(
280
- "A Cronjob object that use for validate and generate datetime.",
281
+ "A Cronjob object that use for validate and generate datetime."
281
282
  ),
282
283
  ),
283
284
  ]
284
285
 
285
286
  @field_validator(
286
- "cronjob", mode="before", json_schema_input_type=Union[CronJobYear, str]
287
+ "cronjob",
288
+ mode="before",
289
+ json_schema_input_type=Union[CronJobYear, str],
287
290
  )
288
291
  def __prepare_cronjob(
289
- cls, value: str | CronJobYear, info: ValidationInfo
292
+ cls, value: Union[CronJobYear, str], info: ValidationInfo
290
293
  ) -> CronJobYear:
291
294
  """Prepare crontab value that able to receive with string type.
292
295
  This step will get options kwargs from extras field and pass to the
@@ -9,7 +9,7 @@ annotate for handle error only.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Literal, Optional, TypedDict, overload
12
+ from typing import Literal, Optional, TypedDict, Union, overload
13
13
 
14
14
 
15
15
  class ErrorData(TypedDict):
@@ -55,8 +55,9 @@ class BaseWorkflowException(Exception):
55
55
 
56
56
  def to_dict(
57
57
  self, with_refs: bool = False
58
- ) -> ErrorData | dict[str, ErrorData]:
59
- """Return ErrorData data from the current exception object.
58
+ ) -> Union[ErrorData, dict[str, ErrorData]]:
59
+ """Return ErrorData data from the current exception object. If with_refs
60
+ flag was set, it will return mapping of refs and itself data.
60
61
 
61
62
  :rtype: ErrorData
62
63
  """
ddeutil/workflow/job.py CHANGED
@@ -39,7 +39,7 @@ from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
39
39
  from pydantic.functional_validators import field_validator, model_validator
40
40
  from typing_extensions import Self
41
41
 
42
- from .__types import DictData, DictStr, Matrix
42
+ from .__types import DictData, DictStr, Matrix, StrOrNone
43
43
  from .exceptions import (
44
44
  JobException,
45
45
  StageException,
@@ -329,14 +329,14 @@ class Job(BaseModel):
329
329
  ... }
330
330
  """
331
331
 
332
- id: Optional[str] = Field(
332
+ id: StrOrNone = Field(
333
333
  default=None,
334
334
  description=(
335
335
  "A job ID that was set from Workflow model after initialize step. "
336
336
  "If this model create standalone, it will be None."
337
337
  ),
338
338
  )
339
- desc: Optional[str] = Field(
339
+ desc: StrOrNone = Field(
340
340
  default=None,
341
341
  description="A job description that can be markdown syntax.",
342
342
  )
@@ -345,7 +345,7 @@ class Job(BaseModel):
345
345
  description="A target node for this job to use for execution.",
346
346
  alias="runs-on",
347
347
  )
348
- condition: Optional[str] = Field(
348
+ condition: StrOrNone = Field(
349
349
  default=None,
350
350
  description="A job condition statement to allow job executable.",
351
351
  alias="if",
@@ -526,7 +526,7 @@ class Job(BaseModel):
526
526
  output: DictData,
527
527
  to: DictData,
528
528
  *,
529
- job_id: Optional[str] = None,
529
+ job_id: StrOrNone = None,
530
530
  ) -> DictData:
531
531
  """Set an outputs from execution result context to the received context
532
532
  with a `to` input parameter. The result context from job strategy
@@ -567,7 +567,7 @@ class Job(BaseModel):
567
567
  :param output: (DictData) A result data context that want to extract
568
568
  and transfer to the `strategies` key in receive context.
569
569
  :param to: (DictData) A received context data.
570
- :param job_id: (Optional[str]) A job ID if the `id` field does not set.
570
+ :param job_id: (StrOrNone) A job ID if the `id` field does not set.
571
571
 
572
572
  :rtype: DictData
573
573
  """
@@ -607,8 +607,8 @@ class Job(BaseModel):
607
607
  self,
608
608
  params: DictData,
609
609
  *,
610
- run_id: Optional[str] = None,
611
- parent_run_id: Optional[str] = None,
610
+ run_id: StrOrNone = None,
611
+ parent_run_id: StrOrNone = None,
612
612
  event: Optional[Event] = None,
613
613
  ) -> Result:
614
614
  """Job execution with passing dynamic parameters from the workflow
@@ -800,8 +800,8 @@ def local_execute(
800
800
  job: Job,
801
801
  params: DictData,
802
802
  *,
803
- run_id: Optional[str] = None,
804
- parent_run_id: Optional[str] = None,
803
+ run_id: StrOrNone = None,
804
+ parent_run_id: StrOrNone = None,
805
805
  event: Optional[Event] = None,
806
806
  ) -> Result:
807
807
  """Local job execution with passing dynamic parameters from the workflow
@@ -919,8 +919,8 @@ def self_hosted_execute(
919
919
  job: Job,
920
920
  params: DictData,
921
921
  *,
922
- run_id: Optional[str] = None,
923
- parent_run_id: Optional[str] = None,
922
+ run_id: StrOrNone = None,
923
+ parent_run_id: StrOrNone = None,
924
924
  event: Optional[Event] = None,
925
925
  ) -> Result: # pragma: no cov
926
926
  """Self-Hosted job execution with passing dynamic parameters from the
@@ -982,8 +982,8 @@ def azure_batch_execute(
982
982
  job: Job,
983
983
  params: DictData,
984
984
  *,
985
- run_id: Optional[str] = None,
986
- parent_run_id: Optional[str] = None,
985
+ run_id: StrOrNone = None,
986
+ parent_run_id: StrOrNone = None,
987
987
  event: Optional[Event] = None,
988
988
  ) -> Result: # pragma: no cov
989
989
  """Azure Batch job execution that will run all job's stages on the Azure
@@ -1036,8 +1036,8 @@ def docker_execution(
1036
1036
  job: Job,
1037
1037
  params: DictData,
1038
1038
  *,
1039
- run_id: Optional[str] = None,
1040
- parent_run_id: Optional[str] = None,
1039
+ run_id: StrOrNone = None,
1040
+ parent_run_id: StrOrNone = None,
1041
1041
  event: Optional[Event] = None,
1042
1042
  ): # pragma: no cov
1043
1043
  """Docker job execution.
ddeutil/workflow/logs.py CHANGED
@@ -4,13 +4,17 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  # [x] Use fix config for `get_logger`, and Model initialize step.
7
- """A Logs module contain Trace dataclass and Audit Pydantic model.
7
+ """A Logs module contain Trace and Audit Pydantic models for process log from
8
+ the core workflow engine. I separate part of log to 2 types:
9
+ - Trace: A stdout and stderr log
10
+ - Audit: An audit release log for tracking incremental running workflow.
8
11
  """
9
12
  from __future__ import annotations
10
13
 
11
14
  import json
12
15
  import logging
13
16
  import os
17
+ import re
14
18
  from abc import ABC, abstractmethod
15
19
  from collections.abc import Iterator
16
20
  from datetime import datetime
@@ -18,6 +22,7 @@ from functools import lru_cache
18
22
  from inspect import Traceback, currentframe, getframeinfo
19
23
  from pathlib import Path
20
24
  from threading import get_ident
25
+ from types import FrameType
21
26
  from typing import ClassVar, Literal, Optional, TypeVar, Union
22
27
 
23
28
  from pydantic import BaseModel, ConfigDict, Field
@@ -28,12 +33,14 @@ from .__types import DictData
28
33
  from .conf import config, dynamic
29
34
  from .utils import cut_id, get_dt_now, prepare_newline
30
35
 
36
+ METADATA: str = "metadata.json"
37
+
31
38
 
32
39
  @lru_cache
33
40
  def get_logger(name: str):
34
41
  """Return logger object with an input module name.
35
42
 
36
- :param name: A module name that want to log.
43
+ :param name: (str) A module name that want to log.
37
44
  """
38
45
  lg = logging.getLogger(name)
39
46
 
@@ -68,13 +75,55 @@ def get_dt_tznow() -> datetime: # pragma: no cov
68
75
 
69
76
 
70
77
  PREFIX_LOGS: dict[str, dict] = {
71
- "CALLER": {"emoji": ""},
72
- "STAGE": {"emoji": ""},
73
- "JOB": {"emoji": ""},
74
- "WORKFLOW": {"emoji": "🏃"},
75
- "RELEASE": {"emoji": ""},
76
- "POKE": {"emoji": ""},
78
+ "CALLER": {
79
+ "emoji": "📍",
80
+ "desc": "logs from any usage from custom caller function.",
81
+ },
82
+ "STAGE": {"emoji": "⚙️", "desc": "logs from stages module."},
83
+ "JOB": {"emoji": "⛓️", "desc": "logs from job module."},
84
+ "WORKFLOW": {"emoji": "🏃", "desc": "logs from workflow module."},
85
+ "RELEASE": {"emoji": "📅", "desc": "logs from release workflow method."},
86
+ "POKING": {"emoji": "⏰", "desc": "logs from poke workflow method."},
77
87
  } # pragma: no cov
88
+ PREFIX_DEFAULT: str = "CALLER"
89
+ PREFIX_LOGS_REGEX: re.Pattern[str] = re.compile(
90
+ rf"(^\[(?P<name>{'|'.join(PREFIX_LOGS)})]:\s?)?(?P<message>.*)",
91
+ re.MULTILINE | re.DOTALL | re.ASCII | re.VERBOSE,
92
+ ) # pragma: no cov
93
+
94
+
95
+ class PrefixMsg(BaseModel):
96
+ """Prefix Message model for receive grouping dict from searching prefix data
97
+ from logging message.
98
+ """
99
+
100
+ name: Optional[str] = Field(default=None)
101
+ message: Optional[str] = Field(default=None)
102
+
103
+ def prepare(self, extras: Optional[DictData] = None) -> str:
104
+ """Prepare message with force add prefix before writing trace log.
105
+
106
+ :rtype: str
107
+ """
108
+ name: str = self.name or PREFIX_DEFAULT
109
+ emoji: str = (
110
+ f"{PREFIX_LOGS[name]['emoji']} "
111
+ if (extras or {}).get("log_add_emoji", True)
112
+ else ""
113
+ )
114
+ return f"{emoji}[{name}]: {self.message}"
115
+
116
+
117
+ def extract_msg_prefix(msg: str) -> PrefixMsg:
118
+ """Extract message prefix from an input message.
119
+
120
+ :param msg: A message that want to extract.
121
+
122
+ :rtype: PrefixMsg
123
+ """
124
+ return PrefixMsg.model_validate(
125
+ obj=PREFIX_LOGS_REGEX.search(msg).groupdict()
126
+ )
78
127
 
79
128
 
80
129
  class TraceMeta(BaseModel): # pragma: no cov
@@ -91,6 +140,28 @@ class TraceMeta(BaseModel): # pragma: no cov
91
140
  filename: str = Field(description="A filename of this log.")
92
141
  lineno: int = Field(description="A line number of this log.")
93
142
 
143
+ @classmethod
144
+ def dynamic_frame(
145
+ cls, frame: FrameType, *, extras: Optional[DictData] = None
146
+ ) -> Traceback:
147
+ """Dynamic Frame information base on the `logs_trace_frame_layer` config
148
+ value that was set from the extra parameter.
149
+
150
+ :param frame: (FrameType) The current frame that want to dynamic.
151
+ :param extras: (DictData) An extra parameter that want to get the
152
+ `logs_trace_frame_layer` config value.
153
+ """
154
+ extras: DictData = extras or {}
155
+ layer: int = extras.get("logs_trace_frame_layer", 4)
156
+ for _ in range(layer):
157
+ _frame: Optional[FrameType] = frame.f_back
158
+ if _frame is None:
159
+ raise ValueError(
160
+ f"Layer value does not valid, the maximum frame is: {_ + 1}"
161
+ )
162
+ frame: FrameType = _frame
163
+ return getframeinfo(frame)
164
+
94
165
  @classmethod
95
166
  def make(
96
167
  cls,
@@ -100,7 +171,8 @@ class TraceMeta(BaseModel): # pragma: no cov
100
171
  *,
101
172
  extras: Optional[DictData] = None,
102
173
  ) -> Self:
103
- """Make the current TraceMeta instance that catching local state.
174
+ """Make the current metric for contract this TraceMeta model instance
175
+ that will catch local states like PID, thread identity.
104
176
 
105
177
  :param mode: (Literal["stdout", "stderr"]) A metadata mode.
106
178
  :param message: (str) A message.
@@ -110,9 +182,8 @@ class TraceMeta(BaseModel): # pragma: no cov
110
182
 
111
183
  :rtype: Self
112
184
  """
113
- frame_info: Traceback = getframeinfo(
114
- currentframe().f_back.f_back.f_back
115
- )
185
+ frame: FrameType = currentframe()
186
+ frame_info: Traceback = cls.dynamic_frame(frame, extras=extras)
116
187
  extras: DictData = extras or {}
117
188
  return cls(
118
189
  mode=mode,
@@ -157,13 +228,11 @@ class TraceData(BaseModel): # pragma: no cov
157
228
  if (file / f"{mode}.txt").exists():
158
229
  data[mode] = (file / f"{mode}.txt").read_text(encoding="utf-8")
159
230
 
160
- if (file / "metadata.json").exists():
231
+ if (file / METADATA).exists():
161
232
  data["meta"] = [
162
233
  json.loads(line)
163
234
  for line in (
164
- (file / "metadata.json")
165
- .read_text(encoding="utf-8")
166
- .splitlines()
235
+ (file / METADATA).read_text(encoding="utf-8").splitlines()
167
236
  )
168
237
  ]
169
238
 
@@ -263,7 +332,9 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
263
332
 
264
333
  :param message: (str) A message that want to log.
265
334
  """
266
- msg: str = prepare_newline(self.make_message(message))
335
+ msg: str = prepare_newline(
336
+ self.make_message(extract_msg_prefix(message).prepare(self.extras))
337
+ )
267
338
 
268
339
  if mode != "debug" or (
269
340
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -320,7 +391,9 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
320
391
 
321
392
  :param message: (str) A message that want to log.
322
393
  """
323
- msg: str = prepare_newline(self.make_message(message))
394
+ msg: str = prepare_newline(
395
+ self.make_message(extract_msg_prefix(message).prepare(self.extras))
396
+ )
324
397
 
325
398
  if mode != "debug" or (
326
399
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -468,7 +541,7 @@ class FileTrace(BaseTrace): # pragma: no cov
468
541
 
469
542
  mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
470
543
  trace_meta: TraceMeta = TraceMeta.make(
471
- mode=mode, level=level, message=message
544
+ mode=mode, level=level, message=message, extras=self.extras
472
545
  )
473
546
 
474
547
  with (self.pointer / f"{mode}.txt").open(
@@ -477,9 +550,7 @@ class FileTrace(BaseTrace): # pragma: no cov
477
550
  fmt: str = dynamic("log_format_file", extras=self.extras)
478
551
  f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
479
552
 
480
- with (self.pointer / "metadata.json").open(
481
- mode="at", encoding="utf-8"
482
- ) as f:
553
+ with (self.pointer / METADATA).open(mode="at", encoding="utf-8") as f:
483
554
  f.write(trace_meta.model_dump_json() + "\n")
484
555
 
485
556
  async def awriter(
@@ -496,7 +567,7 @@ class FileTrace(BaseTrace): # pragma: no cov
496
567
 
497
568
  mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
498
569
  trace_meta: TraceMeta = TraceMeta.make(
499
- mode=mode, level=level, message=message
570
+ mode=mode, level=level, message=message, extras=self.extras
500
571
  )
501
572
 
502
573
  async with aiofiles.open(
@@ -506,7 +577,7 @@ class FileTrace(BaseTrace): # pragma: no cov
506
577
  await f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
507
578
 
508
579
  async with aiofiles.open(
509
- self.pointer / "metadata.json", mode="at", encoding="utf-8"
580
+ self.pointer / METADATA, mode="at", encoding="utf-8"
510
581
  ) as f:
511
582
  await f.write(trace_meta.model_dump_json() + "\n")
512
583
 
@@ -38,7 +38,7 @@ class Status(IntEnum):
38
38
  CANCEL = 4
39
39
 
40
40
  @property
41
- def emoji(self) -> str:
41
+ def emoji(self) -> str: # pragma: no cov
42
42
  """Return the emoji value of this status.
43
43
 
44
44
  :rtype: str
@@ -58,7 +58,7 @@ from pydantic import BaseModel, Field
58
58
  from pydantic.functional_validators import model_validator
59
59
  from typing_extensions import Self
60
60
 
61
- from .__types import DictData, DictStr, StrOrInt, TupleStr
61
+ from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
62
62
  from .conf import dynamic
63
63
  from .exceptions import StageException, to_dict
64
64
  from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
@@ -87,7 +87,7 @@ class BaseStage(BaseModel, ABC):
87
87
  default_factory=dict,
88
88
  description="An extra parameter that override core config values.",
89
89
  )
90
- id: Optional[str] = Field(
90
+ id: StrOrNone = Field(
91
91
  default=None,
92
92
  description=(
93
93
  "A stage ID that use to keep execution output or getting by job "
@@ -97,7 +97,7 @@ class BaseStage(BaseModel, ABC):
97
97
  name: str = Field(
98
98
  description="A stage name that want to logging when start execution.",
99
99
  )
100
- condition: Optional[str] = Field(
100
+ condition: StrOrNone = Field(
101
101
  default=None,
102
102
  description=(
103
103
  "A stage condition statement to allow stage executable. This field "
@@ -162,8 +162,8 @@ class BaseStage(BaseModel, ABC):
162
162
  self,
163
163
  params: DictData,
164
164
  *,
165
- run_id: Optional[str] = None,
166
- parent_run_id: Optional[str] = None,
165
+ run_id: StrOrNone = None,
166
+ parent_run_id: StrOrNone = None,
167
167
  result: Optional[Result] = None,
168
168
  event: Optional[Event] = None,
169
169
  raise_error: Optional[bool] = None,
@@ -411,8 +411,8 @@ class BaseAsyncStage(BaseStage):
411
411
  self,
412
412
  params: DictData,
413
413
  *,
414
- run_id: Optional[str] = None,
415
- parent_run_id: Optional[str] = None,
414
+ run_id: StrOrNone = None,
415
+ parent_run_id: StrOrNone = None,
416
416
  result: Optional[Result] = None,
417
417
  event: Optional[Event] = None,
418
418
  raise_error: Optional[bool] = None,
@@ -469,7 +469,7 @@ class EmptyStage(BaseAsyncStage):
469
469
  ... }
470
470
  """
471
471
 
472
- echo: Optional[str] = Field(
472
+ echo: StrOrNone = Field(
473
473
  default=None,
474
474
  description="A message that want to show on the stdout.",
475
475
  )
@@ -598,14 +598,14 @@ class BashStage(BaseAsyncStage):
598
598
  )
599
599
 
600
600
  @contextlib.asynccontextmanager
601
- async def acreate_sh_file(
602
- self, bash: str, env: DictStr, run_id: Optional[str] = None
601
+ async def async_create_sh_file(
602
+ self, bash: str, env: DictStr, run_id: StrOrNone = None
603
603
  ) -> AsyncIterator[TupleStr]:
604
604
  """Async create and write `.sh` file with the `aiofiles` package.
605
605
 
606
606
  :param bash: (str) A bash statement.
607
607
  :param env: (DictStr) An environment variable that set before run bash.
608
- :param run_id: (Optional[str]) A running stage ID that use for writing sh
608
+ :param run_id: (StrOrNone) A running stage ID that use for writing sh
609
609
  file instead generate by UUID4.
610
610
 
611
611
  :rtype: AsyncIterator[TupleStr]
@@ -635,14 +635,14 @@ class BashStage(BaseAsyncStage):
635
635
 
636
636
  @contextlib.contextmanager
637
637
  def create_sh_file(
638
- self, bash: str, env: DictStr, run_id: Optional[str] = None
638
+ self, bash: str, env: DictStr, run_id: StrOrNone = None
639
639
  ) -> Iterator[TupleStr]:
640
640
  """Create and write the `.sh` file before giving this file name to
641
641
  context. After that, it will auto delete this file automatic.
642
642
 
643
643
  :param bash: (str) A bash statement.
644
644
  :param env: (DictStr) An environment variable that set before run bash.
645
- :param run_id: (Optional[str]) A running stage ID that use for writing sh
645
+ :param run_id: (StrOrNone) A running stage ID that use for writing sh
646
646
  file instead generate by UUID4.
647
647
 
648
648
  :rtype: Iterator[TupleStr]
@@ -752,7 +752,7 @@ class BashStage(BaseAsyncStage):
752
752
  dedent(self.bash.strip("\n")), params, extras=self.extras
753
753
  )
754
754
 
755
- async with self.acreate_sh_file(
755
+ async with self.async_create_sh_file(
756
756
  bash=bash,
757
757
  env=param2template(self.env, params, extras=self.extras),
758
758
  run_id=result.run_id,
@@ -1294,11 +1294,12 @@ class TriggerStage(BaseStage):
1294
1294
  extras=self.extras | {"stage_raise_error": True},
1295
1295
  ).execute(
1296
1296
  params=param2template(self.params, params, extras=self.extras),
1297
- parent_run_id=result.run_id,
1297
+ run_id=None,
1298
+ parent_run_id=result.parent_run_id,
1298
1299
  event=event,
1299
1300
  )
1300
1301
  if rs.status == FAILED:
1301
- err_msg: Optional[str] = (
1302
+ err_msg: StrOrNone = (
1302
1303
  f" with:\n{msg}"
1303
1304
  if (msg := rs.context.get("errors", {}).get("message"))
1304
1305
  else "."
@@ -1826,7 +1827,10 @@ class UntilStage(BaseNestedStage):
1826
1827
  ... "stages": [
1827
1828
  ... {
1828
1829
  ... "name": "Start increase item value.",
1829
- ... "run": "item = ${{ item }}\\nitem += 1\\n"
1830
+ ... "run": (
1831
+ ... "item = ${{ item }}\\n"
1832
+ ... "item += 1\\n"
1833
+ ... )
1830
1834
  ... },
1831
1835
  ... ],
1832
1836
  ... }
@@ -2215,9 +2219,7 @@ class CaseStage(BaseNestedStage):
2215
2219
  extras=self.extras,
2216
2220
  )
2217
2221
 
2218
- _case: Optional[str] = param2template(
2219
- self.case, params, extras=self.extras
2220
- )
2222
+ _case: StrOrNone = param2template(self.case, params, extras=self.extras)
2221
2223
 
2222
2224
  result.trace.info(f"[STAGE]: Execute Case-Stage: {_case!r}.")
2223
2225
  _else: Optional[Match] = None
@@ -2396,8 +2398,14 @@ class DockerStage(BaseStage): # pragma: no cov
2396
2398
 
2397
2399
  :rtype: Result
2398
2400
  """
2399
- from docker import DockerClient
2400
- from docker.errors import ContainerError
2401
+ try:
2402
+ from docker import DockerClient
2403
+ from docker.errors import ContainerError
2404
+ except ImportError:
2405
+ raise ImportError(
2406
+ "Docker stage need the docker package, you should install it "
2407
+ "by `pip install docker` first."
2408
+ ) from None
2401
2409
 
2402
2410
  client = DockerClient(
2403
2411
  base_url="unix://var/run/docker.sock", version="auto"
@@ -2459,7 +2467,7 @@ class DockerStage(BaseStage): # pragma: no cov
2459
2467
  exit_status,
2460
2468
  None,
2461
2469
  f"{self.image}:{self.tag}",
2462
- out,
2470
+ out.decode("utf-8"),
2463
2471
  )
2464
2472
  output_file: Path = Path(f".docker.{result.run_id}.logs/outputs.json")
2465
2473
  if not output_file.exists():
@@ -2518,7 +2526,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
2518
2526
  py: str,
2519
2527
  values: DictData,
2520
2528
  deps: list[str],
2521
- run_id: Optional[str] = None,
2529
+ run_id: StrOrNone = None,
2522
2530
  ) -> Iterator[str]:
2523
2531
  """Create the .py file with an input Python string statement.
2524
2532
 
@@ -2526,7 +2534,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
2526
2534
  :param values: A variable that want to set before running this
2527
2535
  :param deps: An additional Python dependencies that want install before
2528
2536
  run this python stage.
2529
- :param run_id: (Optional[str]) A running ID of this stage execution.
2537
+ :param run_id: (StrOrNone) A running ID of this stage execution.
2530
2538
  """
2531
2539
  run_id: str = run_id or uuid.uuid4()
2532
2540
  f_name: str = f"{run_id}.py"
@@ -2644,5 +2652,8 @@ Stage = Annotated[
2644
2652
  RaiseStage,
2645
2653
  EmptyStage,
2646
2654
  ],
2647
- Field(union_mode="smart"),
2655
+ Field(
2656
+ union_mode="smart",
2657
+ description="A stage models that already implemented on this package.",
2658
+ ),
2648
2659
  ] # pragma: no cov
ddeutil/workflow/utils.py CHANGED
@@ -24,7 +24,7 @@ from .__types import DictData, Matrix
24
24
 
25
25
  T = TypeVar("T")
26
26
  UTC: Final[ZoneInfo] = ZoneInfo("UTC")
27
- MARK_NL: Final[str] = "||"
27
+ MARK_NEWLINE: Final[str] = "||"
28
28
 
29
29
 
30
30
  def prepare_newline(msg: str) -> str:
@@ -34,11 +34,12 @@ def prepare_newline(msg: str) -> str:
34
34
 
35
35
  :rtype: str
36
36
  """
37
- msg: str = msg.strip("\n").replace("\n", MARK_NL)
38
- if MARK_NL not in msg:
37
+ # NOTE: Remove ending with "\n" and replace "\n" with the "||" value.
38
+ msg: str = msg.strip("\n").replace("\n", MARK_NEWLINE)
39
+ if MARK_NEWLINE not in msg:
39
40
  return msg
40
41
 
41
- msg_lines: list[str] = msg.split(MARK_NL)
42
+ msg_lines: list[str] = msg.split(MARK_NEWLINE)
42
43
  msg_last: str = msg_lines[-1]
43
44
  msg_body: str = (
44
45
  "\n" + "\n".join(f" ... | \t{s}" for s in msg_lines[1:-1])
@@ -563,11 +563,12 @@ class Workflow(BaseModel):
563
563
  adding jobs key to this parameter.
564
564
  """
565
565
  # VALIDATE: Incoming params should have keys that set on this workflow.
566
- if check_key := [
566
+ check_key: list[str] = [
567
567
  f"{k!r}"
568
568
  for k in self.params
569
569
  if (k not in params and self.params[k].required)
570
- ]:
570
+ ]
571
+ if check_key:
571
572
  raise WorkflowException(
572
573
  f"Required Param on this workflow setting does not set: "
573
574
  f"{', '.join(check_key)}."
@@ -670,7 +671,7 @@ class Workflow(BaseModel):
670
671
  rs: Result = self.execute(
671
672
  params=values,
672
673
  result=result,
673
- parent_run_id=result.parent_run_id,
674
+ parent_run_id=result.run_id,
674
675
  timeout=timeout,
675
676
  )
676
677
  result.trace.info(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.59
3
+ Version: 0.0.60
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -44,6 +44,8 @@ Requires-Dist: aiofiles; extra == "async"
44
44
  Requires-Dist: aiohttp; extra == "async"
45
45
  Provides-Extra: docker
46
46
  Requires-Dist: docker==7.1.0; extra == "docker"
47
+ Provides-Extra: self-hosted
48
+ Requires-Dist: requests==2.32.3; extra == "self-hosted"
47
49
  Dynamic: license-file
48
50
 
49
51
  # Workflow Orchestration
@@ -1,20 +1,20 @@
1
- ddeutil/workflow/__about__.py,sha256=pgt1UgXVQ5NH2bT0-9YyCLh7xzGOl3WFa4CWgM2rMyE,28
1
+ ddeutil/workflow/__about__.py,sha256=sQSmxiDbXlnTI1qDQGcyxr1EGvwITzIX0PKi2dOg4LU,28
2
2
  ddeutil/workflow/__cron.py,sha256=5DHQKejG-76L_oREW78RcwMzeyKddJxSMmBzYyMAeeY,28536
3
3
  ddeutil/workflow/__init__.py,sha256=NXEhjzKFdIGa-jtIq9HXChLCjSXNPd8VJ8ltggxbBO8,1371
4
4
  ddeutil/workflow/__main__.py,sha256=x-sYedl4T8p6054aySk-EQX6vhytvPR0HvaBNYxMzp0,364
5
- ddeutil/workflow/__types.py,sha256=7xXy6ynpT6Do6U5A-XYSVuinE2g-4wlZGGJ1NACK1BE,4343
5
+ ddeutil/workflow/__types.py,sha256=uNfoRbVmNK5O37UUMVnqcmoghD9oMS1q9fXC0APnjSI,4584
6
6
  ddeutil/workflow/conf.py,sha256=NLvjZ8bpDsn4e0MG3m1vgMdAwtmii5hP1D0STKQyZeo,14907
7
- ddeutil/workflow/event.py,sha256=I9CUFAsqUNguCPALVmqwKWaUHcSpwg2S-chGrTZRXFY,10410
8
- ddeutil/workflow/exceptions.py,sha256=Phe5JK-nLDt1Yh-ilWnpLIJl1VRsAzK4TBZ1tTiv9OQ,2359
9
- ddeutil/workflow/job.py,sha256=2GGW_sY3XhZGYJpXWi84k4uTRV9YMPOMagVtDeeDya8,35289
10
- ddeutil/workflow/logs.py,sha256=BFdPKcIsoSPU-tZBMQvBipAdlBur8IjOk7MxyqrTC8Q,28537
7
+ ddeutil/workflow/event.py,sha256=iAvd7TfAJaMndDhxbi1xNLzl4wNlgLqe1nIseaIm5-Y,10533
8
+ ddeutil/workflow/exceptions.py,sha256=TKHBIlfquz3yEb8_kg6UXpxVLKxstt3QA9a1XYsLPJk,2455
9
+ ddeutil/workflow/job.py,sha256=Php1b3n6c-jddel8PTSa61kAW22QBTetzoLVR4XXM4E,35240
10
+ ddeutil/workflow/logs.py,sha256=81wl83dbYDcMctGmWiptmFaoZoXFO2TS0E4sxOILOQk,31321
11
11
  ddeutil/workflow/params.py,sha256=tBjKe1_e0TlUrSrlMahDuAdNNBlGBAKMmMMQ9eV-YSs,11616
12
- ddeutil/workflow/result.py,sha256=LJieCsaQJOgZKz68wao2XKXCFm3bXl2jNkeHniP_Y90,5888
12
+ ddeutil/workflow/result.py,sha256=4M9VCcveI8Yz6ZrnI-67SZlry-Z8G7e0hziy1k-pklk,5906
13
13
  ddeutil/workflow/reusables.py,sha256=mw_Fi763B5am0EmntcjLBF7MDEhKqud2BYHcYyno5Ec,17663
14
14
  ddeutil/workflow/scheduler.py,sha256=OsEyj2zscQ-3bDMk2z7UtKlCWLlgoGjaRFt17o1B1ew,27263
15
- ddeutil/workflow/stages.py,sha256=_GGrI4sayY1HArqV0aWUMwukONnZ_6-QVAiaomP4nbY,92239
16
- ddeutil/workflow/utils.py,sha256=S4TN1qH6t8NiZfIapJed3ZS35aQc18HzDPQ4oLqct7M,8804
17
- ddeutil/workflow/workflow.py,sha256=gq7zbBeJptqb9rmHcR29c8Gbh43N1-cW94NdHGb6Td4,44856
15
+ ddeutil/workflow/stages.py,sha256=N_DkEUGiwpglovtXx-Wg3zX_03eGBT650zRsZV7knKk,92640
16
+ ddeutil/workflow/utils.py,sha256=ADJTt3kiF44qntsRnOUdCFihlB2WWbRE-Tojp5EOYbk,8898
17
+ ddeutil/workflow/workflow.py,sha256=BFnaB_7mrYZ3KV07AV16xR9khsoSt9i3QLyEtrLNAqs,44877
18
18
  ddeutil/workflow/api/__init__.py,sha256=kY30dL8HPY8tY_GBmm7y_3OdoXzB1-EA2a96PLU0AQw,5278
19
19
  ddeutil/workflow/api/logs.py,sha256=NMTnOnsBrDB5129329xF2myLdrb-z9k1MQrmrP7qXJw,1818
20
20
  ddeutil/workflow/api/utils.py,sha256=uTtUFVLpiYYahXvCVx8sueRQ03K2Xw1id_gW3IMmX1U,5295
@@ -23,9 +23,9 @@ ddeutil/workflow/api/routes/job.py,sha256=8X5VLDJH6PumyNIY6JGRNBsf2gWN0eG9DzxRPS
23
23
  ddeutil/workflow/api/routes/logs.py,sha256=U6vOni3wd-ZTOwd3yVdSOpgyRmNdcgfngU5KlLM3Cww,5383
24
24
  ddeutil/workflow/api/routes/schedules.py,sha256=14RnaJKEGMSJtncI1H_QQVZNBe_jDS40PPRO6qFc3i0,4805
25
25
  ddeutil/workflow/api/routes/workflows.py,sha256=GJu5PiXEylswrXylEImpncySjeU9chrvrtjhiMCw2RQ,4529
26
- ddeutil_workflow-0.0.59.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
27
- ddeutil_workflow-0.0.59.dist-info/METADATA,sha256=JqwaP7hwfaNAlrObHxnHlzDyBDbb_cy186S92GRjXZ4,19343
28
- ddeutil_workflow-0.0.59.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
29
- ddeutil_workflow-0.0.59.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
30
- ddeutil_workflow-0.0.59.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
31
- ddeutil_workflow-0.0.59.dist-info/RECORD,,
26
+ ddeutil_workflow-0.0.60.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
27
+ ddeutil_workflow-0.0.60.dist-info/METADATA,sha256=VixljHKK-7rmiv5UC_65sp2DAgb3kHjms1H7vvF0DyY,19427
28
+ ddeutil_workflow-0.0.60.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
29
+ ddeutil_workflow-0.0.60.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
30
+ ddeutil_workflow-0.0.60.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
31
+ ddeutil_workflow-0.0.60.dist-info/RECORD,,