ddeutil-workflow 0.0.76__py3-none-any.whl → 0.0.78__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.76"
1
+ __version__: str = "0.0.78"
ddeutil/workflow/cli.py CHANGED
@@ -57,7 +57,7 @@ def init() -> None:
57
57
  wf-example:
58
58
  type: Workflow
59
59
  desc: |
60
- An example workflow template.
60
+ An example workflow template that provide the demo of workflow.
61
61
  params:
62
62
  name:
63
63
  type: str
@@ -65,6 +65,10 @@ def init() -> None:
65
65
  jobs:
66
66
  first-job:
67
67
  stages:
68
+
69
+ - name: "Hello Stage"
70
+ echo: "Start say hi to the console"
71
+
68
72
  - name: "Call tasks"
69
73
  uses: tasks/say-hello-func@example
70
74
  with:
@@ -232,7 +236,7 @@ def workflow_json_schema(
232
236
  json_schema = TypeAdapter(template).json_schema(by_alias=True)
233
237
  template_schema: dict[str, str] = {
234
238
  "$schema": "http://json-schema.org/draft-07/schema#",
235
- "title": "Workflow Configuration Schema",
239
+ "title": "Workflow Configuration JSON Schema",
236
240
  "version": __version__,
237
241
  }
238
242
  with open(output, mode="w", encoding="utf-8") as f:
ddeutil/workflow/conf.py CHANGED
@@ -307,8 +307,6 @@ class YamlParser:
307
307
  all_data.append((file_stat.st_mtime, data))
308
308
  elif (t := data.get("type")) and t == obj_type:
309
309
  all_data.append((file_stat.st_mtime, data))
310
- else:
311
- continue
312
310
 
313
311
  return {} if not all_data else max(all_data, key=lambda x: x[0])[1]
314
312
 
@@ -322,25 +320,30 @@ class YamlParser:
322
320
  excluded: Optional[list[str]] = None,
323
321
  extras: Optional[DictData] = None,
324
322
  ignore_filename: Optional[str] = None,
323
+ tags: Optional[list[Union[str, int]]] = None,
325
324
  ) -> Iterator[tuple[str, DictData]]:
326
325
  """Find all data that match with object type in config path. This class
327
326
  method can use include and exclude list of identity name for filter and
328
327
  adds-on.
329
328
 
330
- :param obj: (object | str) An object that want to validate matching
331
- before return.
332
- :param path: (Path) A config path object.
333
- :param paths: (list[Path]) A list of config path object.
334
- :param excluded: An included list of data key that want to filter from
335
- data.
336
- :param extras: (DictData) An extra parameter that use to override core
337
- config values.
338
- :param ignore_filename: (str) An ignore filename. Default is
329
+ Args:
330
+ obj: (object | str) An object that want to validate matching
331
+ before return.
332
+ path: (Path) A config path object.
333
+ paths: (list[Path]) A list of config path object.
334
+ excluded: An included list of data key that want to filter from
335
+ data.
336
+ extras: (DictData) An extra parameter that use to override core
337
+ config values.
338
+ ignore_filename: (str) An ignore filename. Default is
339
339
  ``.confignore`` filename.
340
+ tags: (list[str])
341
+ A list of tag that want to filter.
340
342
 
341
343
  :rtype: Iterator[tuple[str, DictData]]
342
344
  """
343
345
  excluded: list[str] = excluded or []
346
+ tags: list[str] = tags or []
344
347
  path: Path = dynamic("conf_path", f=path, extras=extras)
345
348
  paths: Optional[list[Path]] = paths or (extras or {}).get("conf_paths")
346
349
  if not paths:
@@ -366,7 +369,17 @@ class YamlParser:
366
369
  if key in excluded:
367
370
  continue
368
371
 
372
+ if (
373
+ tags
374
+ and isinstance((ts := data.get("tags", [])), list)
375
+ and any(t not in ts for t in tags)
376
+ ):
377
+ continue
378
+
369
379
  if (t := data.get("type")) and t == obj_type:
380
+ file_stat: os.stat_result = file.lstat()
381
+ data["created_at"] = file_stat.st_ctime
382
+ data["updated_at"] = file_stat.st_mtime
370
383
  marking: tuple[float, DictData] = (
371
384
  file.lstat().st_mtime,
372
385
  data,
@@ -469,7 +482,10 @@ def pass_env(value: T) -> T: # pragma: no cov
469
482
  if isinstance(value, dict):
470
483
  return {k: pass_env(value[k]) for k in value}
471
484
  elif isinstance(value, (list, tuple, set)):
472
- return type(value)([pass_env(i) for i in value])
485
+ try:
486
+ return type(value)(pass_env(i) for i in value)
487
+ except TypeError:
488
+ return value
473
489
  if not isinstance(value, str):
474
490
  return value
475
491
 
@@ -56,13 +56,13 @@ def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
56
56
  ErrorData: Dictionary containing exception name and message
57
57
 
58
58
  Example:
59
- ```python
60
- try:
61
- raise ValueError("Something went wrong")
62
- except Exception as e:
63
- error_data = to_dict(e, context="workflow_execution")
64
- # Returns: {"name": "ValueError", "message": "Something went wrong", "context": "workflow_execution"}
65
- ```
59
+ >>> try:
60
+ >>> raise ValueError("Something went wrong")
61
+ >>> except Exception as e:
62
+ >>> error_data = to_dict(e, context="workflow_execution")
63
+ >>> # Returns: {
64
+ >>> # "name": "ValueError", "message": "Something went wrong", "context": "workflow_execution"
65
+ >>> # }
66
66
  """
67
67
  return {
68
68
  "name": exception.__class__.__name__,
@@ -85,14 +85,12 @@ class BaseError(Exception):
85
85
  params: Parameter data that was being processed when error occurred
86
86
 
87
87
  Example:
88
- ```python
89
- try:
90
- # Some workflow operation
91
- pass
92
- except BaseError as e:
93
- error_dict = e.to_dict(with_refs=True)
94
- print(f"Error in {e.refs}: {error_dict}")
95
- ```
88
+ >>> try:
89
+ >>> # NOTE: Some workflow operation
90
+ >>> pass
91
+ >>> except BaseError as e:
92
+ >>> error_dict = e.to_dict(with_refs=True)
93
+ >>> print(f\"Error in {e.refs}: {error_dict}\")
96
94
  """
97
95
 
98
96
  def __init__(
ddeutil/workflow/event.py CHANGED
@@ -39,7 +39,6 @@ from __future__ import annotations
39
39
  from dataclasses import fields
40
40
  from datetime import datetime
41
41
  from typing import Annotated, Any, Literal, Optional, Union
42
- from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
43
42
 
44
43
  from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
45
44
  from pydantic.functional_serializers import field_serializer
@@ -107,7 +106,7 @@ class BaseCrontab(BaseModel):
107
106
  )
108
107
  tz: TimeZoneName = Field(
109
108
  default="UTC",
110
- description="A timezone string value.",
109
+ description="A timezone string value that will pass to ZoneInfo.",
111
110
  alias="timezone",
112
111
  )
113
112
 
@@ -125,38 +124,25 @@ class BaseCrontab(BaseModel):
125
124
  data["timezone"] = tz
126
125
  return data
127
126
 
128
- @field_validator("tz")
129
- def __validate_tz(cls, value: str) -> str:
130
- """Validate timezone value.
131
-
132
- Args:
133
- value: Timezone string to validate.
134
-
135
- Returns:
136
- Validated timezone string.
137
-
138
- Raises:
139
- ValueError: If timezone is invalid.
140
- """
141
- try:
142
- _ = ZoneInfo(value)
143
- return value
144
- except ZoneInfoNotFoundError as e:
145
- raise ValueError(f"Invalid timezone: {value}") from e
146
-
147
127
 
148
128
  class CrontabValue(BaseCrontab):
149
129
  """Crontab model using interval-based specification.
150
130
 
151
131
  Attributes:
152
- interval: Scheduling interval ('daily', 'weekly', 'monthly').
153
- day: Day specification for weekly/monthly schedules.
132
+ interval: (Interval)
133
+ A scheduling interval string ('daily', 'weekly', 'monthly').
134
+ day: (str, default None)
135
+ Day specification for weekly/monthly schedules.
154
136
  time: Time of day in 'HH:MM' format.
155
137
  """
156
138
 
157
- interval: Interval
139
+ interval: Interval = Field(description="A scheduling interval string.")
158
140
  day: Optional[str] = Field(default=None)
159
- time: str = Field(default="00:00")
141
+ time: str = Field(
142
+ default="00:00",
143
+ pattern=r"\d{2}:\d{2}",
144
+ description="A time of day that pass with format 'HH:MM'.",
145
+ )
160
146
 
161
147
  @property
162
148
  def cronjob(self) -> CronJob:
@@ -182,10 +168,13 @@ class CrontabValue(BaseCrontab):
182
168
  TypeError: If start parameter is neither string nor datetime.
183
169
  """
184
170
  if isinstance(start, str):
185
- start: datetime = datetime.fromisoformat(start)
186
- elif not isinstance(start, datetime):
187
- raise TypeError("start value should be str or datetime type.")
188
- return self.cronjob.schedule(date=start, tz=self.tz)
171
+ return self.cronjob.schedule(
172
+ date=datetime.fromisoformat(start), tz=self.tz
173
+ )
174
+
175
+ if isinstance(start, datetime):
176
+ return self.cronjob.schedule(date=start, tz=self.tz)
177
+ raise TypeError("start value should be str or datetime type.")
189
178
 
190
179
  def next(self, start: Union[str, datetime]) -> CronRunner:
191
180
  """Get next scheduled datetime after given start time.
@@ -222,11 +211,6 @@ class Crontab(BaseCrontab):
222
211
  "A Cronjob object that use for validate and generate datetime."
223
212
  ),
224
213
  )
225
- tz: TimeZoneName = Field(
226
- default="UTC",
227
- description="A timezone string value.",
228
- alias="timezone",
229
- )
230
214
 
231
215
  @model_validator(mode="before")
232
216
  def __prepare_values(cls, data: Any) -> Any:
@@ -376,7 +360,10 @@ Cron = Annotated[
376
360
  ],
377
361
  Field(
378
362
  union_mode="smart",
379
- description="Event model type supporting year-based, standard, and interval-based cron scheduling.",
363
+ description=(
364
+ "Event model type supporting year-based, standard, and "
365
+ "interval-based cron scheduling."
366
+ ),
380
367
  ),
381
368
  ] # pragma: no cov
382
369
 
ddeutil/workflow/job.py CHANGED
@@ -48,10 +48,11 @@ from enum import Enum
48
48
  from functools import lru_cache
49
49
  from textwrap import dedent
50
50
  from threading import Event
51
- from typing import Annotated, Any, Literal, Optional, Union
51
+ from typing import Annotated, Any, Optional, Union
52
52
 
53
53
  from ddeutil.core import freeze_args
54
54
  from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
55
+ from pydantic.functional_serializers import field_serializer
55
56
  from pydantic.functional_validators import field_validator, model_validator
56
57
  from typing_extensions import Self
57
58
 
@@ -263,24 +264,20 @@ class BaseRunsOn(BaseModel): # pragma: no cov
263
264
  object and override execute method.
264
265
  """
265
266
 
266
- type: RunsOn = Field(description="A runs-on type.")
267
+ type: RunsOn = LOCAL
267
268
  args: DictData = Field(
268
269
  default_factory=dict,
269
- alias="with",
270
270
  description=(
271
271
  "An argument that pass to the runs-on execution function. This "
272
272
  "args will override by this child-model with specific args model."
273
273
  ),
274
+ alias="with",
274
275
  )
275
276
 
276
277
 
277
278
  class OnLocal(BaseRunsOn): # pragma: no cov
278
279
  """Runs-on local."""
279
280
 
280
- type: Literal[RunsOn.LOCAL] = Field(
281
- default=RunsOn.LOCAL, validate_default=True
282
- )
283
-
284
281
 
285
282
  class SelfHostedArgs(BaseModel):
286
283
  """Self-Hosted arguments."""
@@ -292,9 +289,7 @@ class SelfHostedArgs(BaseModel):
292
289
  class OnSelfHosted(BaseRunsOn): # pragma: no cov
293
290
  """Runs-on self-hosted."""
294
291
 
295
- type: Literal[RunsOn.SELF_HOSTED] = Field(
296
- default=RunsOn.SELF_HOSTED, validate_default=True
297
- )
292
+ type: RunsOn = SELF_HOSTED
298
293
  args: SelfHostedArgs = Field(alias="with")
299
294
 
300
295
 
@@ -310,9 +305,7 @@ class AzBatchArgs(BaseModel):
310
305
 
311
306
  class OnAzBatch(BaseRunsOn): # pragma: no cov
312
307
 
313
- type: Literal[RunsOn.AZ_BATCH] = Field(
314
- default=RunsOn.AZ_BATCH, validate_default=True
315
- )
308
+ type: RunsOn = AZ_BATCH
316
309
  args: AzBatchArgs = Field(alias="with")
317
310
 
318
311
 
@@ -331,23 +324,21 @@ class DockerArgs(BaseModel):
331
324
  class OnDocker(BaseRunsOn): # pragma: no cov
332
325
  """Runs-on Docker container."""
333
326
 
334
- type: Literal[RunsOn.DOCKER] = Field(
335
- default=RunsOn.DOCKER, validate_default=True
336
- )
337
- args: DockerArgs = Field(alias="with", default_factory=DockerArgs)
327
+ type: RunsOn = DOCKER
328
+ args: DockerArgs = Field(default_factory=DockerArgs, alias="with")
338
329
 
339
330
 
340
331
  def get_discriminator_runs_on(model: dict[str, Any]) -> RunsOn:
341
332
  """Get discriminator of the RunsOn models."""
342
333
  t: str = model.get("type")
343
- return RunsOn(t) if t else RunsOn.LOCAL
334
+ return RunsOn(t) if t else LOCAL
344
335
 
345
336
 
346
337
  RunsOnModel = Annotated[
347
338
  Union[
348
- Annotated[OnSelfHosted, Tag(RunsOn.SELF_HOSTED)],
349
- Annotated[OnDocker, Tag(RunsOn.DOCKER)],
350
- Annotated[OnLocal, Tag(RunsOn.LOCAL)],
339
+ Annotated[OnSelfHosted, Tag(SELF_HOSTED)],
340
+ Annotated[OnDocker, Tag(DOCKER)],
341
+ Annotated[OnLocal, Tag(LOCAL)],
351
342
  ],
352
343
  Discriminator(get_discriminator_runs_on),
353
344
  ]
@@ -490,6 +481,10 @@ class Job(BaseModel):
490
481
 
491
482
  return self
492
483
 
484
+ @field_serializer("runs_on")
485
+ def __serialize_runs_on(self, value: RunsOnModel):
486
+ return value.model_dump(by_alias=True)
487
+
493
488
  def stage(self, stage_id: str) -> Stage:
494
489
  """Return stage instance that exists in this job via passing an input
495
490
  stage ID.
@@ -20,7 +20,6 @@ Functions:
20
20
  from __future__ import annotations
21
21
 
22
22
  from dataclasses import field
23
- from datetime import datetime
24
23
  from enum import Enum
25
24
  from typing import Optional, Union
26
25
 
@@ -42,7 +41,7 @@ from . import (
42
41
  from .__types import DictData
43
42
  from .audits import Trace, get_trace
44
43
  from .errors import ResultError
45
- from .utils import default_gen_id, get_dt_now
44
+ from .utils import default_gen_id
46
45
 
47
46
 
48
47
  class Status(str, Enum):
@@ -105,8 +104,9 @@ def validate_statuses(statuses: list[Status]) -> Status:
105
104
  """Determine final status from multiple status values.
106
105
 
107
106
  Applies workflow logic to determine the overall status based on a collection
108
- of individual status values. Follows priority order: CANCEL > FAILED > WAIT >
109
- individual status consistency.
107
+ of individual status values. Follows priority order:
108
+
109
+ CANCEL > FAILED > WAIT > individual status consistency.
110
110
 
111
111
  Args:
112
112
  statuses: List of status values to evaluate
@@ -132,7 +132,7 @@ def validate_statuses(statuses: list[Status]) -> Status:
132
132
  for status in (SUCCESS, SKIP):
133
133
  if all(s == status for s in statuses):
134
134
  return status
135
- return FAILED if FAILED in statuses else SUCCESS
135
+ return SUCCESS
136
136
 
137
137
 
138
138
  def get_status_from_error(
@@ -166,10 +166,6 @@ def get_status_from_error(
166
166
  return FAILED
167
167
 
168
168
 
169
- def default_context() -> DictData:
170
- return {"status": WAIT}
171
-
172
-
173
169
  @dataclass(
174
170
  config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True),
175
171
  )
@@ -186,14 +182,13 @@ class Result:
186
182
  field that keep dict value change its ID when update new value to it.
187
183
  """
188
184
 
185
+ extras: DictData = field(default_factory=dict, compare=False, repr=False)
189
186
  status: Status = field(default=WAIT)
190
- context: DictData = field(default_factory=default_context)
187
+ context: Optional[DictData] = field(default=None)
191
188
  info: DictData = field(default_factory=dict)
192
- run_id: Optional[str] = field(default_factory=default_gen_id)
189
+ run_id: str = field(default_factory=default_gen_id)
193
190
  parent_run_id: Optional[str] = field(default=None)
194
- ts: datetime = field(default_factory=get_dt_now, compare=False)
195
191
  trace: Optional[Trace] = field(default=None, compare=False, repr=False)
196
- extras: DictData = field(default_factory=dict, compare=False, repr=False)
197
192
 
198
193
  @model_validator(mode="after")
199
194
  def __prepare_trace(self) -> Self:
@@ -207,20 +202,18 @@ class Result:
207
202
  parent_run_id=self.parent_run_id,
208
203
  extras=self.extras,
209
204
  )
210
- return self
211
-
212
- def set_parent_run_id(self, running_id: str) -> Self:
213
- """Set a parent running ID.
214
205
 
215
- :param running_id: (str) A running ID that want to update on this model.
206
+ return self
216
207
 
217
- :rtype: Self
218
- """
219
- self.parent_run_id: str = running_id
220
- self.trace: Trace = get_trace(
221
- self.run_id, parent_run_id=running_id, extras=self.extras
208
+ @classmethod
209
+ def from_trace(cls, trace: Trace):
210
+ """Construct the result model from trace for clean code objective."""
211
+ return cls(
212
+ run_id=trace.run_id,
213
+ parent_run_id=trace.parent_run_id,
214
+ extras=trace.extras,
215
+ trace=trace,
222
216
  )
223
- return self
224
217
 
225
218
  def catch(
226
219
  self,
@@ -237,7 +230,11 @@ class Result:
237
230
 
238
231
  :rtype: Self
239
232
  """
240
- self.__dict__["context"].update(context or {})
233
+ if self.__dict__["context"] is None:
234
+ self.__dict__["context"] = context
235
+ else:
236
+ self.__dict__["context"].update(context or {})
237
+
241
238
  self.__dict__["status"] = (
242
239
  Status(status) if isinstance(status, int) else status
243
240
  )
@@ -262,13 +259,6 @@ class Result:
262
259
  self.__dict__["info"].update(data)
263
260
  return self
264
261
 
265
- def alive_time(self) -> float: # pragma: no cov
266
- """Return total seconds that this object use since it was created.
267
-
268
- :rtype: float
269
- """
270
- return (get_dt_now() - self.ts).total_seconds()
271
-
272
262
 
273
263
  def catch(
274
264
  context: DictData,
@@ -276,7 +266,13 @@ def catch(
276
266
  updated: DictData | None = None,
277
267
  **kwargs,
278
268
  ) -> DictData:
279
- """Catch updated context to the current context."""
269
+ """Catch updated context to the current context.
270
+
271
+ Args:
272
+ context: A context data that want to be the current context.
273
+ status: A status enum object.
274
+ updated: A updated data that will update to the current context.
275
+ """
280
276
  context.update(updated or {})
281
277
  context["status"] = Status(status) if isinstance(status, int) else status
282
278
 
@@ -289,7 +285,7 @@ def catch(
289
285
  context[k].update(kwargs[k])
290
286
  # NOTE: Exclude the `info` key for update information data.
291
287
  elif k == "info":
292
- context["info"].update(kwargs["info"])
288
+ context.update({"info": kwargs["info"]})
293
289
  else:
294
290
  raise ResultError(f"The key {k!r} does not exists on context data.")
295
291
  return context
@@ -421,12 +421,13 @@ def param2template(
421
421
  for k in value
422
422
  }
423
423
  elif isinstance(value, (list, tuple, set)):
424
- return type(value)(
425
- [
424
+ try:
425
+ return type(value)(
426
426
  param2template(i, params, context, filters, extras=extras)
427
427
  for i in value
428
- ]
429
- )
428
+ )
429
+ except TypeError:
430
+ return value
430
431
  elif not isinstance(value, str):
431
432
  return value
432
433
  return str2template(
@@ -598,7 +599,7 @@ def make_registry(
598
599
  if not (
599
600
  hasattr(func, "tag")
600
601
  and hasattr(func, "name")
601
- and str(getattr(func, "mark", "NOT SET")) == "tag"
602
+ and str(getattr(func, "mark", "NOTSET")) == "tag"
602
603
  ): # pragma: no cov
603
604
  continue
604
605
 
@@ -616,6 +617,7 @@ def make_registry(
616
617
  f"{module}.{submodule}, you should change this tag name or "
617
618
  f"change it func name."
618
619
  )
620
+
619
621
  rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
620
622
 
621
623
  return rs
@@ -77,7 +77,15 @@ from pathlib import Path
77
77
  from subprocess import CompletedProcess
78
78
  from textwrap import dedent
79
79
  from threading import Event
80
- from typing import Annotated, Any, Optional, TypeVar, Union, get_type_hints
80
+ from typing import (
81
+ Annotated,
82
+ Any,
83
+ Callable,
84
+ Optional,
85
+ TypeVar,
86
+ Union,
87
+ get_type_hints,
88
+ )
81
89
 
82
90
  from ddeutil.core import str2list
83
91
  from pydantic import BaseModel, Field, ValidationError
@@ -177,7 +185,7 @@ class BaseStage(BaseModel, ABC):
177
185
  "A stage description that use to logging when start execution."
178
186
  ),
179
187
  )
180
- condition: StrOrNone = Field(
188
+ condition: Optional[Union[str, bool]] = Field(
181
189
  default=None,
182
190
  description=(
183
191
  "A stage condition statement to allow stage executable. This field "
@@ -329,7 +337,7 @@ class BaseStage(BaseModel, ABC):
329
337
  parent_run_id=parent_run_id,
330
338
  event=event,
331
339
  )
332
- if result_caught.status == WAIT:
340
+ if result_caught.status == WAIT: # pragma: no cov
333
341
  raise StageError(
334
342
  "Status from execution should not return waiting status."
335
343
  )
@@ -513,6 +521,9 @@ class BaseStage(BaseModel, ABC):
513
521
  if not self.condition:
514
522
  return False
515
523
 
524
+ if isinstance(self.condition, bool):
525
+ return self.condition
526
+
516
527
  try:
517
528
  # WARNING: The eval build-in function is very dangerous. So, it
518
529
  # should use the `re` module to validate eval-string before
@@ -656,7 +667,7 @@ class BaseAsyncStage(BaseStage, ABC):
656
667
  parent_run_id=parent_run_id,
657
668
  event=event,
658
669
  )
659
- if result_caught.status == WAIT:
670
+ if result_caught.status == WAIT: # pragma: no cov
660
671
  raise StageError(
661
672
  "Status from execution should not return waiting status."
662
673
  )
@@ -1580,15 +1591,23 @@ class CallStage(BaseRetryStage):
1580
1591
 
1581
1592
  :rtype: Any
1582
1593
  """
1583
- if isinstance(value, dict):
1584
- if any(k in value for k in ("result", "extras")):
1585
- raise ValueError(
1586
- "The argument on workflow template for the caller stage "
1587
- "should not pass `result` and `extras`. They are special "
1588
- "arguments."
1589
- )
1594
+ if isinstance(value, dict) and any(
1595
+ k in value for k in ("result", "extras")
1596
+ ):
1597
+ raise ValueError(
1598
+ "The argument on workflow template for the caller stage "
1599
+ "should not pass `result` and `extras`. They are special "
1600
+ "arguments."
1601
+ )
1590
1602
  return value
1591
1603
 
1604
+ def get_caller(self, params: DictData) -> Callable[[], TagFunc]:
1605
+ """Get the lazy TagFuc object from registry."""
1606
+ return extract_call(
1607
+ param2template(self.uses, params, extras=self.extras),
1608
+ registries=self.extras.get("registry_caller"),
1609
+ )
1610
+
1592
1611
  def process(
1593
1612
  self,
1594
1613
  params: DictData,
@@ -1615,11 +1634,7 @@ class CallStage(BaseRetryStage):
1615
1634
  trace: Trace = get_trace(
1616
1635
  run_id, parent_run_id=parent_run_id, extras=self.extras
1617
1636
  )
1618
- call_func: TagFunc = extract_call(
1619
- param2template(self.uses, params, extras=self.extras),
1620
- registries=self.extras.get("registry_caller"),
1621
- )()
1622
-
1637
+ call_func: TagFunc = self.get_caller(params=params)()
1623
1638
  trace.info(f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'")
1624
1639
 
1625
1640
  # VALIDATE: check input task caller parameters that exists before
@@ -1677,7 +1692,7 @@ class CallStage(BaseRetryStage):
1677
1692
  )
1678
1693
 
1679
1694
  args: DictData = self.validate_model_args(
1680
- call_func, args, run_id, parent_run_id
1695
+ call_func, args, run_id, parent_run_id, extras=self.extras
1681
1696
  )
1682
1697
  if inspect.iscoroutinefunction(call_func):
1683
1698
  loop = asyncio.get_event_loop()
@@ -1738,11 +1753,7 @@ class CallStage(BaseRetryStage):
1738
1753
  trace: Trace = get_trace(
1739
1754
  run_id, parent_run_id=parent_run_id, extras=self.extras
1740
1755
  )
1741
- call_func: TagFunc = extract_call(
1742
- param2template(self.uses, params, extras=self.extras),
1743
- registries=self.extras.get("registry_caller"),
1744
- )()
1745
-
1756
+ call_func: TagFunc = self.get_caller(params=params)()
1746
1757
  await trace.ainfo(
1747
1758
  f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'"
1748
1759
  )
@@ -1795,8 +1806,13 @@ class CallStage(BaseRetryStage):
1795
1806
  if "extras" not in sig.parameters and not has_keyword:
1796
1807
  args.pop("extras")
1797
1808
 
1809
+ if event and event.is_set():
1810
+ raise StageCancelError(
1811
+ "Execution was canceled from the event before start parallel."
1812
+ )
1813
+
1798
1814
  args: DictData = self.validate_model_args(
1799
- call_func, args, run_id, parent_run_id
1815
+ call_func, args, run_id, parent_run_id, extras=self.extras
1800
1816
  )
1801
1817
  if inspect.iscoroutinefunction(call_func):
1802
1818
  rs: DictOrModel = await call_func(
@@ -1829,12 +1845,13 @@ class CallStage(BaseRetryStage):
1829
1845
  extras=self.extras,
1830
1846
  )
1831
1847
 
1848
+ @staticmethod
1832
1849
  def validate_model_args(
1833
- self,
1834
1850
  func: TagFunc,
1835
1851
  args: DictData,
1836
1852
  run_id: str,
1837
1853
  parent_run_id: Optional[str] = None,
1854
+ extras: Optional[DictData] = None,
1838
1855
  ) -> DictData:
1839
1856
  """Validate an input arguments before passing to the caller function.
1840
1857
 
@@ -1846,18 +1863,18 @@ class CallStage(BaseRetryStage):
1846
1863
  :rtype: DictData
1847
1864
  """
1848
1865
  try:
1849
- model_instance: BaseModel = create_model_from_caller(
1850
- func
1851
- ).model_validate(args)
1852
- override: DictData = dict(model_instance)
1866
+ override: DictData = dict(
1867
+ create_model_from_caller(func).model_validate(args)
1868
+ )
1853
1869
  args.update(override)
1870
+
1854
1871
  type_hints: dict[str, Any] = get_type_hints(func)
1855
1872
  for arg in type_hints:
1856
1873
 
1857
1874
  if arg == "return":
1858
1875
  continue
1859
1876
 
1860
- if arg.removeprefix("_") in args:
1877
+ if arg.startswith("_") and arg.removeprefix("_") in args:
1861
1878
  args[arg] = args.pop(arg.removeprefix("_"))
1862
1879
  continue
1863
1880
 
@@ -1868,7 +1885,7 @@ class CallStage(BaseRetryStage):
1868
1885
  ) from e
1869
1886
  except TypeError as e:
1870
1887
  trace: Trace = get_trace(
1871
- run_id, parent_run_id=parent_run_id, extras=self.extras
1888
+ run_id, parent_run_id=parent_run_id, extras=extras
1872
1889
  )
1873
1890
  trace.warning(
1874
1891
  f"[STAGE]: Get type hint raise TypeError: {e}, so, it skip "
@@ -2295,7 +2312,13 @@ class ForEachStage(BaseNestedStage):
2295
2312
  ... }
2296
2313
  """
2297
2314
 
2298
- foreach: Union[list[str], list[int], str] = Field(
2315
+ foreach: Union[
2316
+ list[str],
2317
+ list[int],
2318
+ str,
2319
+ dict[str, Any],
2320
+ dict[int, Any],
2321
+ ] = Field(
2299
2322
  description=(
2300
2323
  "A items for passing to stages via ${{ item }} template parameter."
2301
2324
  ),
@@ -2501,7 +2524,7 @@ class ForEachStage(BaseNestedStage):
2501
2524
  run_id, parent_run_id=parent_run_id, extras=self.extras
2502
2525
  )
2503
2526
  event: Event = event or Event()
2504
- foreach: Union[list[str], list[int]] = pass_env(
2527
+ foreach: Union[list[str], list[int], str] = pass_env(
2505
2528
  param2template(self.foreach, params, extras=self.extras)
2506
2529
  )
2507
2530
 
@@ -2516,9 +2539,10 @@ class ForEachStage(BaseNestedStage):
2516
2539
  ) from e
2517
2540
 
2518
2541
  # [VALIDATE]: Type of the foreach should be `list` type.
2519
- elif not isinstance(foreach, list):
2542
+ elif isinstance(foreach, dict):
2520
2543
  raise TypeError(
2521
- f"Does not support foreach: {foreach!r} ({type(foreach)})"
2544
+ f"Does not support dict foreach: {foreach!r} ({type(foreach)}) "
2545
+ f"yet."
2522
2546
  )
2523
2547
  # [Validate]: Value in the foreach item should not be duplicate when the
2524
2548
  # `use_index_as_key` field did not set.
@@ -32,6 +32,7 @@ Example:
32
32
  >>> trace = get_trace("running-id-101", parent_run_id="workflow-001")
33
33
  >>> trace.info("Workflow execution started")
34
34
  >>> trace.debug("Processing stage 1")
35
+
35
36
  """
36
37
  from __future__ import annotations
37
38
 
@@ -848,7 +849,7 @@ def get_trace(
848
849
  :rtype: Trace
849
850
  """
850
851
  # NOTE: Allow you to override trace model by the extra parameter.
851
- map_trace_models: dict[str, type[Trace]] = extras.get(
852
+ map_trace_models: dict[str, type[Trace]] = (extras or {}).get(
852
853
  "trace_model_mapping", {}
853
854
  )
854
855
  url: ParseResult
ddeutil/workflow/utils.py CHANGED
@@ -247,7 +247,7 @@ def default_gen_id() -> str:
247
247
 
248
248
  :rtype: str
249
249
  """
250
- return gen_id("manual", unique=True)
250
+ return gen_id("MOCK", unique=True)
251
251
 
252
252
 
253
253
  def make_exec(path: Union[Path, str]) -> None:
@@ -271,7 +271,10 @@ def filter_func(value: T) -> T:
271
271
  if isinstance(value, dict):
272
272
  return {k: filter_func(value[k]) for k in value}
273
273
  elif isinstance(value, (list, tuple, set)):
274
- return type(value)([filter_func(i) for i in value])
274
+ try:
275
+ return type(value)(filter_func(i) for i in value)
276
+ except TypeError:
277
+ return value
275
278
 
276
279
  if isfunction(value):
277
280
  # NOTE: If it wants to improve to get this function, it is able to save
@@ -338,7 +341,10 @@ def dump_all(
338
341
  if isinstance(value, dict):
339
342
  return {k: dump_all(value[k], by_alias=by_alias) for k in value}
340
343
  elif isinstance(value, (list, tuple, set)):
341
- return type(value)([dump_all(i, by_alias=by_alias) for i in value])
344
+ try:
345
+ return type(value)(dump_all(i, by_alias=by_alias) for i in value)
346
+ except TypeError:
347
+ return value
342
348
  elif isinstance(value, BaseModel):
343
349
  return value.model_dump(by_alias=by_alias)
344
350
  return value
@@ -346,7 +352,7 @@ def dump_all(
346
352
 
347
353
  def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
348
354
  if not obj:
349
- obj_type: Optional[str] = None
355
+ return None
350
356
  elif isinstance(obj, str):
351
357
  obj_type: str = obj
352
358
  elif isclass(obj):
@@ -145,13 +145,17 @@ class Workflow(BaseModel):
145
145
  description="A parameters that need to use on this workflow.",
146
146
  )
147
147
  on: Event = Field(
148
- default_factory=list,
148
+ default_factory=Event,
149
149
  description="An events for this workflow.",
150
150
  )
151
151
  jobs: dict[str, Job] = Field(
152
152
  default_factory=dict,
153
153
  description="A mapping of job ID and job model that already loaded.",
154
154
  )
155
+ tags: list[str] = Field(
156
+ default_factory=list,
157
+ description="A list of tag that use for simple grouping workflow.",
158
+ )
155
159
  created_at: datetime = Field(
156
160
  default_factory=get_dt_now,
157
161
  description=(
@@ -207,11 +211,6 @@ class Workflow(BaseModel):
207
211
  ```
208
212
  """
209
213
  load: YamlParser = YamlParser(name, path=path, extras=extras, obj=cls)
210
-
211
- # NOTE: Validate the config type match with current connection model
212
- if load.type != cls.__name__:
213
- raise ValueError(f"Type {load.type} does not match with {cls}")
214
-
215
214
  data: DictData = copy.deepcopy(load.data)
216
215
  data["name"] = name
217
216
 
@@ -285,6 +284,50 @@ class Workflow(BaseModel):
285
284
 
286
285
  return self
287
286
 
287
+ def detail(self) -> DictData: # pragma: no cov
288
+ """Return the detail of this workflow for generate markdown."""
289
+ return self.model_dump(by_alias=True)
290
+
291
+ def md(self, author: Optional[str] = None) -> str: # pragma: no cov
292
+ """Generate the markdown template."""
293
+
294
+ def align_newline(value: str) -> str:
295
+ return value.rstrip("\n").replace("\n", "\n ")
296
+
297
+ info: str = (
298
+ f"| Author: {author or 'nobody'} "
299
+ f"| created_at: `{self.created_at:%Y-%m-%d %H:%M:%S}` "
300
+ f"| updated_at: `{self.updated_dt:%Y-%m-%d %H:%M:%S}` |\n"
301
+ f"| --- | --- | --- |"
302
+ )
303
+ jobs: str = ""
304
+ for job in self.jobs:
305
+ job_model: Job = self.jobs[job]
306
+ jobs += f"### {job}\n{job_model.desc or ''}\n"
307
+ stags: str = ""
308
+ for stage_model in job_model.stages:
309
+ stags += (
310
+ f"#### {stage_model.name}\n\n"
311
+ f"Stage ID: {stage_model.id or ''}\n"
312
+ f"Stage Model: {stage_model.__class__.__name__}\n\n"
313
+ )
314
+ jobs += f"{stags}\n"
315
+ return dedent(
316
+ f"""
317
+ # Workflow: {self.name}\n
318
+ {align_newline(info)}\n
319
+ {align_newline(self.desc)}\n
320
+ ## Parameters\n
321
+ | name | type | default | description |
322
+ | --- | --- | --- | : --- : |
323
+
324
+ ## Jobs\n
325
+ {align_newline(jobs)}
326
+ """.lstrip(
327
+ "\n"
328
+ )
329
+ )
330
+
288
331
  def job(self, name: str) -> Job:
289
332
  """Return the workflow's Job model that getting by an input job's name
290
333
  or job's ID. This method will pass an extra parameter from this model
@@ -373,7 +416,7 @@ class Workflow(BaseModel):
373
416
  dt = dt.replace(tzinfo=UTC)
374
417
 
375
418
  release: datetime = replace_sec(dt.astimezone(UTC))
376
- if not self.on:
419
+ if not self.on.schedule:
377
420
  return release
378
421
 
379
422
  for on in self.on.schedule:
@@ -867,7 +910,7 @@ class Workflow(BaseModel):
867
910
  event: Optional[ThreadEvent] = None,
868
911
  timeout: float = 3600,
869
912
  max_job_parallel: int = 2,
870
- ) -> Result:
913
+ ) -> Result: # pragma: no cov
871
914
  """Re-Execute workflow with passing the error context data.
872
915
 
873
916
  :param context: A context result that get the failed status.
@@ -896,12 +939,9 @@ class Workflow(BaseModel):
896
939
  "[WORKFLOW]: Does not rerun because it already executed with "
897
940
  "success status."
898
941
  )
899
- return Result(
900
- run_id=run_id,
901
- parent_run_id=parent_run_id,
942
+ return Result.from_trace(trace).catch(
902
943
  status=SUCCESS,
903
944
  context=catch(context=context, status=SUCCESS),
904
- extras=self.extras,
905
945
  )
906
946
 
907
947
  err: dict[str, str] = context.get("errors", {})
@@ -917,12 +957,9 @@ class Workflow(BaseModel):
917
957
  )
918
958
  if not self.jobs:
919
959
  trace.warning(f"[WORKFLOW]: {self.name!r} does not set jobs")
920
- return Result(
921
- run_id=run_id,
922
- parent_run_id=parent_run_id,
960
+ return Result.from_trace(trace).catch(
923
961
  status=SUCCESS,
924
962
  context=catch(context=context, status=SUCCESS),
925
- extras=self.extras,
926
963
  )
927
964
 
928
965
  # NOTE: Prepare the new context variable for rerun process.
@@ -947,12 +984,9 @@ class Workflow(BaseModel):
947
984
  "[WORKFLOW]: It does not have job to rerun. it will change "
948
985
  "status to skip."
949
986
  )
950
- return Result(
951
- run_id=run_id,
952
- parent_run_id=parent_run_id,
987
+ return Result.from_trace(trace).catch(
953
988
  status=SKIP,
954
989
  context=catch(context=context, status=SKIP),
955
- extras=self.extras,
956
990
  )
957
991
 
958
992
  not_timeout_flag: bool = True
@@ -965,9 +999,7 @@ class Workflow(BaseModel):
965
999
 
966
1000
  catch(context, status=WAIT)
967
1001
  if event and event.is_set():
968
- return Result(
969
- run_id=run_id,
970
- parent_run_id=parent_run_id,
1002
+ return Result.from_trace(trace).catch(
971
1003
  status=CANCEL,
972
1004
  context=catch(
973
1005
  context,
@@ -979,7 +1011,6 @@ class Workflow(BaseModel):
979
1011
  ).to_dict(),
980
1012
  },
981
1013
  ),
982
- extras=self.extras,
983
1014
  )
984
1015
 
985
1016
  with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
@@ -1007,9 +1038,7 @@ class Workflow(BaseModel):
1007
1038
  backoff_sleep = 0.01
1008
1039
 
1009
1040
  if check == FAILED: # pragma: no cov
1010
- return Result(
1011
- run_id=run_id,
1012
- parent_run_id=parent_run_id,
1041
+ return Result.from_trace(trace).catch(
1013
1042
  status=FAILED,
1014
1043
  context=catch(
1015
1044
  context,
@@ -1022,7 +1051,6 @@ class Workflow(BaseModel):
1022
1051
  ).to_dict(),
1023
1052
  },
1024
1053
  ),
1025
- extras=self.extras,
1026
1054
  )
1027
1055
  elif check == SKIP: # pragma: no cov
1028
1056
  trace.info(
@@ -1098,12 +1126,9 @@ class Workflow(BaseModel):
1098
1126
  statuses[total + 1 + skip_count + i] = s
1099
1127
 
1100
1128
  st: Status = validate_statuses(statuses)
1101
- return Result(
1102
- run_id=run_id,
1103
- parent_run_id=parent_run_id,
1129
+ return Result.from_trace(trace).catch(
1104
1130
  status=st,
1105
1131
  context=catch(context, status=st),
1106
- extras=self.extras,
1107
1132
  )
1108
1133
 
1109
1134
  event.set()
@@ -1117,9 +1142,7 @@ class Workflow(BaseModel):
1117
1142
 
1118
1143
  time.sleep(0.0025)
1119
1144
 
1120
- return Result(
1121
- run_id=run_id,
1122
- parent_run_id=parent_run_id,
1145
+ return Result.from_trace(trace).catch(
1123
1146
  status=FAILED,
1124
1147
  context=catch(
1125
1148
  context,
@@ -1131,5 +1154,4 @@ class Workflow(BaseModel):
1131
1154
  ).to_dict(),
1132
1155
  },
1133
1156
  ),
1134
- extras=self.extras,
1135
1157
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.76
3
+ Version: 0.0.78
4
4
  Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -147,7 +147,6 @@ For comprehensive API documentation, examples, and best practices:
147
147
  - **[Full Documentation](https://ddeutils.github.io/ddeutil-workflow/)** - Complete user guide and API reference
148
148
  - **[Getting Started](https://ddeutils.github.io/ddeutil-workflow/getting-started/)** - Quick start guide
149
149
  - **[API Reference](https://ddeutils.github.io/ddeutil-workflow/api/workflow/)** - Detailed API documentation
150
- - **[Examples](https://ddeutils.github.io/ddeutil-workflow/examples/)** - Real-world usage examples
151
150
 
152
151
  ## 🎯 Usage
153
152
 
@@ -0,0 +1,30 @@
1
+ ddeutil/workflow/__about__.py,sha256=9g_DJubkaHxt4W_0-r1PmUT5x3bCo_72c-f60EDtlq8,28
2
+ ddeutil/workflow/__cron.py,sha256=avOagaHl9xXOmizeRWm13cOrty9Tw0vRjFq-xoEgpAY,29167
3
+ ddeutil/workflow/__init__.py,sha256=_8sP-CTPOfwsFFhmdwQ2Gp7yY7qJemP7TYsIWgd5jc0,3300
4
+ ddeutil/workflow/__main__.py,sha256=Qd-f8z2Q2vpiEP2x6PBFsJrpACWDVxFKQk820MhFmHo,59
5
+ ddeutil/workflow/__types.py,sha256=tA2vsr6mzTSzbWB1sb62c5GgxODlfVRz6FvgLNJtQao,4788
6
+ ddeutil/workflow/audits.py,sha256=wANG0jEQ7slUSgVZG4JbjlR5PtmF8mHpM9RH-zpYM_g,12679
7
+ ddeutil/workflow/cli.py,sha256=WnUkxqs2hCc5JVuTvWuEGKp8_EcS_wmhVvSXDhj0eEM,7544
8
+ ddeutil/workflow/conf.py,sha256=7yMSBVh2W9KcDOZxQjeabJZizB_3ydCLICo4JI0syWU,16892
9
+ ddeutil/workflow/errors.py,sha256=UpUIqoyqkvzqjuxtUQ9535l1HeAsyh-plEG0PgDVR2w,5541
10
+ ddeutil/workflow/event.py,sha256=6d5_UvnPI8xRLcX_5wptmvWoXUIGs_JZbjq7khz5oYE,13355
11
+ ddeutil/workflow/job.py,sha256=4_IxtoIwl1qVuSaNngelYNkqQl9ZOa-PHdKCnXvXu0M,43943
12
+ ddeutil/workflow/params.py,sha256=Cyz142OcvENIZrM7Efc2xuGPmmFBhROifP5ojoaCezg,13658
13
+ ddeutil/workflow/result.py,sha256=Xi07E3WuMHS1jLcJg7p4DPuaMFGp0yEDaWCJRotOH6g,8921
14
+ ddeutil/workflow/reusables.py,sha256=pbCHsEl2V3jGWDRcGyxDvGN5rP5kaRxZNgv9x6-pINQ,23338
15
+ ddeutil/workflow/stages.py,sha256=ocsk64duS4BHEOLT3Agkx-fbY6iYmygxCaQhwp6YyyM,122482
16
+ ddeutil/workflow/traces.py,sha256=h7oDlb4Q8LJUp0pste2dWJYOqEaN64KsLTNMfeRUqx8,28475
17
+ ddeutil/workflow/utils.py,sha256=vSGdpaFgQ5vUPxWvVfbNC2__tu5q16B9mhx1BRbEuJo,10968
18
+ ddeutil/workflow/workflow.py,sha256=0KaRCTAipPvvmi9SOC7T0V0CAusgebCM6qju0ethGF0,42409
19
+ ddeutil/workflow/api/__init__.py,sha256=5DzYL3ngceoRshh5HYCSVWChqNJSiP01E1bEd8XxPi0,4799
20
+ ddeutil/workflow/api/log_conf.py,sha256=WfS3udDLSyrP-C80lWOvxxmhd_XWKvQPkwDqKblcH3E,1834
21
+ ddeutil/workflow/api/routes/__init__.py,sha256=JRaJZB0D6mgR17MbZo8yLtdYDtD62AA8MdKlFqhG84M,420
22
+ ddeutil/workflow/api/routes/job.py,sha256=-lbZ_hS9pEdSy6zeke5qrXEgdNxtQ2w9in7cHuM2Jzs,2536
23
+ ddeutil/workflow/api/routes/logs.py,sha256=RiZ62eQVMWArPHE3lpan955U4DdLLkethlvSMlwF7Mg,5312
24
+ ddeutil/workflow/api/routes/workflows.py,sha256=1Mqx4Hft4uJglgJI-Wcw-JzkhomFYZrtP0DnQDBkAFQ,4410
25
+ ddeutil_workflow-0.0.78.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
26
+ ddeutil_workflow-0.0.78.dist-info/METADATA,sha256=2phJ3JRH9o0Sbr22iOo1YeQBr8nyYx6areM2rwNp_Eg,15681
27
+ ddeutil_workflow-0.0.78.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
+ ddeutil_workflow-0.0.78.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
29
+ ddeutil_workflow-0.0.78.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
30
+ ddeutil_workflow-0.0.78.dist-info/RECORD,,
@@ -1,30 +0,0 @@
1
- ddeutil/workflow/__about__.py,sha256=ZWCr4NXnXaxhYuhduVXp9CkXr2m6FgZDruUsw8AdJBM,28
2
- ddeutil/workflow/__cron.py,sha256=avOagaHl9xXOmizeRWm13cOrty9Tw0vRjFq-xoEgpAY,29167
3
- ddeutil/workflow/__init__.py,sha256=_8sP-CTPOfwsFFhmdwQ2Gp7yY7qJemP7TYsIWgd5jc0,3300
4
- ddeutil/workflow/__main__.py,sha256=Qd-f8z2Q2vpiEP2x6PBFsJrpACWDVxFKQk820MhFmHo,59
5
- ddeutil/workflow/__types.py,sha256=tA2vsr6mzTSzbWB1sb62c5GgxODlfVRz6FvgLNJtQao,4788
6
- ddeutil/workflow/audits.py,sha256=wANG0jEQ7slUSgVZG4JbjlR5PtmF8mHpM9RH-zpYM_g,12679
7
- ddeutil/workflow/cli.py,sha256=eAwRZMSEJu-NONc_un0D_1swFlENMjl3C-iXYnyTTPY,7411
8
- ddeutil/workflow/conf.py,sha256=UCw6v2GFD3tA2LRbp7vLifXniey0P5Ef0U9eBPknrWk,16267
9
- ddeutil/workflow/errors.py,sha256=tHS6ekxBmZ6sIeLaxHHSaMfhVvlWnndfb2-Aq-bL2So,5509
10
- ddeutil/workflow/event.py,sha256=siChcBhsu4ejzW1fK0tjHPXQVaSUCSxPYDgDrh6duwo,13676
11
- ddeutil/workflow/job.py,sha256=_NOPWPs2FuiMvNE-L6c9mpXEChXmgQ8zmD33ZzqVi0A,44146
12
- ddeutil/workflow/params.py,sha256=Cyz142OcvENIZrM7Efc2xuGPmmFBhROifP5ojoaCezg,13658
13
- ddeutil/workflow/result.py,sha256=Fz6y6apivLW-94gAxcT42z-mGqWMk6-O3RJ2GGSNUHM,9146
14
- ddeutil/workflow/reusables.py,sha256=q_OA-oifCGIhW_5j6hTZXZk7FBOmDt0xVrtNnscJfNg,23294
15
- ddeutil/workflow/stages.py,sha256=Z6quvhJ5WZPnItd4xOoQyR_KWE2Z6LYWa5d49N0R5D8,121936
16
- ddeutil/workflow/traces.py,sha256=0n6Mytp6oeNjOV8lIsFitzZ6TrtuSNVFkUmodBiE_vA,28466
17
- ddeutil/workflow/utils.py,sha256=EXhIuWzOJHvlcoAdyvuDUomGtMTIB59HxOLpj2VJ1bI,10857
18
- ddeutil/workflow/workflow.py,sha256=Yw7xuEIwQ61qhGfElky9AZY_1o_1Gqta4B1x1nwQNJs,41475
19
- ddeutil/workflow/api/__init__.py,sha256=5DzYL3ngceoRshh5HYCSVWChqNJSiP01E1bEd8XxPi0,4799
20
- ddeutil/workflow/api/log_conf.py,sha256=WfS3udDLSyrP-C80lWOvxxmhd_XWKvQPkwDqKblcH3E,1834
21
- ddeutil/workflow/api/routes/__init__.py,sha256=JRaJZB0D6mgR17MbZo8yLtdYDtD62AA8MdKlFqhG84M,420
22
- ddeutil/workflow/api/routes/job.py,sha256=-lbZ_hS9pEdSy6zeke5qrXEgdNxtQ2w9in7cHuM2Jzs,2536
23
- ddeutil/workflow/api/routes/logs.py,sha256=RiZ62eQVMWArPHE3lpan955U4DdLLkethlvSMlwF7Mg,5312
24
- ddeutil/workflow/api/routes/workflows.py,sha256=1Mqx4Hft4uJglgJI-Wcw-JzkhomFYZrtP0DnQDBkAFQ,4410
25
- ddeutil_workflow-0.0.76.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
26
- ddeutil_workflow-0.0.76.dist-info/METADATA,sha256=zq63qmHeFG1DpJJWKyO5IjA6tMbYQnHNWa8E5OESD5w,15781
27
- ddeutil_workflow-0.0.76.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
28
- ddeutil_workflow-0.0.76.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
29
- ddeutil_workflow-0.0.76.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
30
- ddeutil_workflow-0.0.76.dist-info/RECORD,,