ddeutil-workflow 0.0.76__tar.gz → 0.0.78__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/PKG-INFO +1 -2
  2. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/README.md +0 -1
  3. ddeutil_workflow-0.0.78/src/ddeutil/workflow/__about__.py +1 -0
  4. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/cli.py +6 -2
  5. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/conf.py +28 -12
  6. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/errors.py +13 -15
  7. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/event.py +22 -35
  8. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/job.py +16 -21
  9. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/result.py +30 -34
  10. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/reusables.py +7 -5
  11. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/stages.py +58 -34
  12. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/traces.py +2 -1
  13. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/utils.py +10 -4
  14. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/workflow.py +58 -36
  15. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -2
  16. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_conf.py +145 -15
  17. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_event.py +26 -10
  18. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_result.py +39 -4
  19. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_reusables_call_tag.py +12 -0
  20. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_reusables_template.py +6 -0
  21. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_utils.py +4 -0
  22. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_workflow.py +28 -0
  23. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_workflow_exec_job.py +17 -1
  24. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_workflow_release.py +2 -0
  25. ddeutil_workflow-0.0.76/src/ddeutil/workflow/__about__.py +0 -1
  26. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/LICENSE +0 -0
  27. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/pyproject.toml +0 -0
  28. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/setup.cfg +0 -0
  29. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/__cron.py +0 -0
  30. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/__init__.py +0 -0
  31. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/__main__.py +0 -0
  32. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/__types.py +0 -0
  33. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/__init__.py +0 -0
  34. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/log_conf.py +0 -0
  35. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  36. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/routes/job.py +0 -0
  37. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  38. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  39. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/audits.py +0 -0
  40. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil/workflow/params.py +0 -0
  41. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  42. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  43. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  44. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  45. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  46. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test__cron.py +0 -0
  47. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test__regex.py +0 -0
  48. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_audits.py +0 -0
  49. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_cli.py +0 -0
  50. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_errors.py +0 -0
  51. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_job.py +0 -0
  52. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_job_exec.py +0 -0
  53. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_job_exec_strategy.py +0 -0
  54. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_params.py +0 -0
  55. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_reusables_func_model.py +0 -0
  56. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_reusables_template_filter.py +0 -0
  57. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_strategy.py +0 -0
  58. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_traces.py +0 -0
  59. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_workflow_exec.py +0 -0
  60. {ddeutil_workflow-0.0.76 → ddeutil_workflow-0.0.78}/tests/test_workflow_rerun.py +0 -0
@@ -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
 
@@ -105,7 +105,6 @@ For comprehensive API documentation, examples, and best practices:
105
105
  - **[Full Documentation](https://ddeutils.github.io/ddeutil-workflow/)** - Complete user guide and API reference
106
106
  - **[Getting Started](https://ddeutils.github.io/ddeutil-workflow/getting-started/)** - Quick start guide
107
107
  - **[API Reference](https://ddeutils.github.io/ddeutil-workflow/api/workflow/)** - Detailed API documentation
108
- - **[Examples](https://ddeutils.github.io/ddeutil-workflow/examples/)** - Real-world usage examples
109
108
 
110
109
  ## 🎯 Usage
111
110
 
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.78"
@@ -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:
@@ -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__(
@@ -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
 
@@ -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