ddeutil-workflow 0.0.81__tar.gz → 0.0.82__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 (66) hide show
  1. {ddeutil_workflow-0.0.81/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.82}/PKG-INFO +1 -1
  2. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/pyproject.toml +0 -1
  3. ddeutil_workflow-0.0.82/src/ddeutil/workflow/__about__.py +2 -0
  4. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__init__.py +19 -6
  5. ddeutil_workflow-0.0.81/src/ddeutil/workflow/cli.py → ddeutil_workflow-0.0.82/src/ddeutil/workflow/__main__.py +3 -4
  6. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/job.py +2 -2
  7. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/logs.py +8 -61
  8. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/audits.py +46 -17
  9. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/conf.py +45 -25
  10. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/errors.py +9 -0
  11. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/job.py +70 -16
  12. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/result.py +33 -11
  13. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/stages.py +172 -134
  14. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/traces.py +64 -24
  15. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/utils.py +7 -4
  16. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/workflow.py +66 -75
  17. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  18. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -1
  19. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_audits.py +6 -5
  20. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_cli.py +1 -1
  21. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_conf.py +24 -4
  22. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job.py +7 -0
  23. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job_exec.py +59 -0
  24. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_result.py +11 -0
  25. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_traces.py +19 -13
  26. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_utils.py +2 -4
  27. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec.py +175 -2
  28. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_release.py +65 -5
  29. ddeutil_workflow-0.0.81/src/ddeutil/workflow/__about__.py +0 -1
  30. ddeutil_workflow-0.0.81/src/ddeutil/workflow/__main__.py +0 -4
  31. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/LICENSE +0 -0
  32. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/README.md +0 -0
  33. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/setup.cfg +0 -0
  34. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__cron.py +0 -0
  35. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/__types.py +0 -0
  36. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/__init__.py +0 -0
  37. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/log_conf.py +0 -0
  38. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  39. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  40. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/event.py +0 -0
  41. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/params.py +0 -0
  42. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/__init__.py +0 -0
  43. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
  44. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
  45. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
  46. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
  47. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
  48. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil/workflow/reusables.py +0 -0
  49. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  50. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  51. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  52. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  53. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test__cron.py +0 -0
  54. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test__regex.py +0 -0
  55. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_errors.py +0 -0
  56. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_event.py +0 -0
  57. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_job_exec_strategy.py +0 -0
  58. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_params.py +0 -0
  59. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_call_tag.py +0 -0
  60. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_func_model.py +0 -0
  61. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_template.py +0 -0
  62. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_reusables_template_filter.py +0 -0
  63. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_strategy.py +0 -0
  64. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow.py +0 -0
  65. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/tests/test_workflow_exec_job.py +0 -0
  66. {ddeutil_workflow-0.0.81 → ddeutil_workflow-0.0.82}/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.81
3
+ Version: 0.0.82
4
4
  Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -91,7 +91,6 @@ omit = [
91
91
  "src/ddeutil/workflow/__cron.py",
92
92
  "src/ddeutil/workflow/__main__.py",
93
93
  "src/ddeutil/workflow/__types.py",
94
- "src/ddeutil/workflow/cli.py",
95
94
  "src/ddeutil/workflow/api/__init__.py",
96
95
  "src/ddeutil/workflow/api/log_conf.py",
97
96
  "src/ddeutil/workflow/api/routes/__init__.py",
@@ -0,0 +1,2 @@
1
+ __version__: str = "0.0.82"
2
+ __python_version__: str = "3.9"
@@ -50,11 +50,25 @@ Note:
50
50
  from .__cron import CronRunner
51
51
  from .__types import DictData, DictStr, Matrix, Re, TupleStr
52
52
  from .audits import (
53
+ EVENT,
54
+ FORCE,
55
+ NORMAL,
56
+ RERUN,
53
57
  Audit,
54
58
  FileAudit,
55
59
  get_audit,
56
60
  )
57
- from .conf import *
61
+ from .conf import (
62
+ PREFIX,
63
+ CallerSecret,
64
+ Config,
65
+ YamlParser,
66
+ api_config,
67
+ config,
68
+ dynamic,
69
+ env,
70
+ pass_env,
71
+ )
58
72
  from .errors import (
59
73
  BaseError,
60
74
  JobCancelError,
@@ -63,6 +77,9 @@ from .errors import (
63
77
  ResultError,
64
78
  StageCancelError,
65
79
  StageError,
80
+ StageNestedCancelError,
81
+ StageNestedError,
82
+ StageNestedSkipError,
66
83
  StageSkipError,
67
84
  UtilError,
68
85
  WorkflowCancelError,
@@ -132,15 +149,11 @@ from .stages import (
132
149
  VirtualPyStage,
133
150
  )
134
151
  from .traces import (
135
- TraceManager,
152
+ Trace,
136
153
  get_trace,
137
154
  )
138
155
  from .utils import *
139
156
  from .workflow import (
140
- EVENT,
141
- FORCE,
142
- NORMAL,
143
- RERUN,
144
157
  ReleaseType,
145
158
  Workflow,
146
159
  )
@@ -238,13 +238,12 @@ def workflow_execute(
238
238
  typer.echo(f"... with params: {params_dict}")
239
239
 
240
240
 
241
- WORKFLOW_TYPE = Literal["Workflow"]
242
-
243
-
244
241
  class WorkflowSchema(Workflow):
245
242
  """Override workflow model fields for generate JSON schema file."""
246
243
 
247
- type: WORKFLOW_TYPE = Field(description="A type of workflow template.")
244
+ type: Literal["Workflow"] = Field(
245
+ description="A type of workflow template that should be `Workflow`."
246
+ )
248
247
  name: Optional[str] = Field(default=None, description="A workflow name.")
249
248
  params: dict[str, Union[Param, str]] = Field(
250
249
  default_factory=dict,
@@ -15,7 +15,7 @@ from fastapi.responses import UJSONResponse
15
15
  from ...__types import DictData
16
16
  from ...errors import JobError
17
17
  from ...job import Job
18
- from ...traces import TraceManager, get_trace
18
+ from ...traces import Trace, get_trace
19
19
  from ...utils import gen_id
20
20
 
21
21
  logger = logging.getLogger("uvicorn.error")
@@ -41,7 +41,7 @@ async def job_execute(
41
41
  if extras:
42
42
  job.extras = extras
43
43
 
44
- trace: TraceManager = get_trace(
44
+ trace: Trace = get_trace(
45
45
  run_id, parent_run_id=parent_run_id, extras=job.extras
46
46
  )
47
47
 
@@ -3,7 +3,7 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """This route include audit and trace log paths."""
6
+ """This route include audit log path."""
7
7
  from __future__ import annotations
8
8
 
9
9
  from fastapi import APIRouter, Path, Query
@@ -11,7 +11,6 @@ from fastapi import status as st
11
11
  from fastapi.responses import UJSONResponse
12
12
 
13
13
  from ...audits import get_audit
14
- from ...result import Result
15
14
 
16
15
  router = APIRouter(
17
16
  prefix="/logs",
@@ -20,63 +19,6 @@ router = APIRouter(
20
19
  )
21
20
 
22
21
 
23
- @router.get(
24
- path="/traces/",
25
- response_class=UJSONResponse,
26
- status_code=st.HTTP_200_OK,
27
- summary="Read all trace logs.",
28
- tags=["trace"],
29
- )
30
- async def get_traces(
31
- offset: int = Query(default=0, gt=0),
32
- limit: int = Query(default=100, gt=0),
33
- ):
34
- """Return all trace logs from the current trace log path that config with
35
- `WORKFLOW_LOG_PATH` environment variable name.
36
- """
37
- result = Result()
38
- return {
39
- "message": (
40
- f"Getting trace logs with offset: {offset} and limit: {limit}"
41
- ),
42
- "traces": [
43
- trace.model_dump(
44
- by_alias=True,
45
- exclude_none=True,
46
- exclude_unset=True,
47
- )
48
- for trace in result.trace.find_traces()
49
- ],
50
- }
51
-
52
-
53
- @router.get(
54
- path="/traces/{run_id}",
55
- response_class=UJSONResponse,
56
- status_code=st.HTTP_200_OK,
57
- summary="Read trace log with specific running ID.",
58
- tags=["trace"],
59
- )
60
- async def get_trace_with_id(run_id: str):
61
- """Return trace log with specific running ID from the current trace log path
62
- that config with `WORKFLOW_LOG_PATH` environment variable name.
63
-
64
- - **run_id**: A running ID that want to search a trace log from the log
65
- path.
66
- """
67
- result = Result()
68
- return {
69
- "message": f"Getting trace log with specific running ID: {run_id}",
70
- "trace": (
71
- result.trace.find_trace_with_id(run_id).model_dump(
72
- by_alias=True,
73
- exclude_none=True,
74
- exclude_unset=True,
75
- )
76
- ),
77
- }
78
-
79
-
80
22
  @router.get(
81
23
  path="/audits/",
82
24
  response_class=UJSONResponse,
@@ -84,12 +26,17 @@ async def get_trace_with_id(run_id: str):
84
26
  summary="Read all audit logs.",
85
27
  tags=["audit"],
86
28
  )
87
- async def get_audits():
29
+ async def get_audits(
30
+ offset: int = Query(default=0, gt=0),
31
+ limit: int = Query(default=100, gt=0),
32
+ ):
88
33
  """Return all audit logs from the current audit log path that config with
89
34
  `WORKFLOW_AUDIT_URL` environment variable name.
90
35
  """
91
36
  return {
92
- "message": "Getting audit logs",
37
+ "message": (
38
+ f"Getting audit logs with offset: {offset} and limit: {limit}",
39
+ ),
93
40
  "audits": list(get_audit().find_audits(name="demo")),
94
41
  }
95
42
 
@@ -49,33 +49,65 @@ import zlib
49
49
  from abc import ABC, abstractmethod
50
50
  from collections.abc import Iterator
51
51
  from datetime import datetime, timedelta
52
+ from enum import Enum
52
53
  from pathlib import Path
53
54
  from typing import Annotated, Any, ClassVar, Literal, Optional, Union
54
55
  from urllib.parse import ParseResult, urlparse
55
56
 
56
- from pydantic import BaseModel, Field, TypeAdapter
57
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
57
58
  from pydantic.functional_validators import field_validator, model_validator
58
59
  from typing_extensions import Self
59
60
 
60
61
  from .__types import DictData
61
62
  from .conf import dynamic
62
- from .traces import TraceManager, get_trace, set_logging
63
+ from .traces import Trace, get_trace, set_logging
63
64
 
64
65
  logger = logging.getLogger("ddeutil.workflow")
65
66
 
66
67
 
68
+ class ReleaseType(str, Enum):
69
+ """Release type enumeration for workflow execution modes.
70
+
71
+ This enum defines the different types of workflow releases that can be
72
+ triggered, each with specific behavior and use cases.
73
+
74
+ Attributes:
75
+ NORMAL: Standard workflow release execution
76
+ RERUN: Re-execution of previously failed workflow
77
+ EVENT: Event-triggered workflow execution
78
+ FORCE: Forced execution bypassing normal conditions
79
+ """
80
+
81
+ NORMAL = "normal"
82
+ RERUN = "rerun"
83
+ EVENT = "event"
84
+ FORCE = "force"
85
+
86
+
87
+ NORMAL = ReleaseType.NORMAL
88
+ RERUN = ReleaseType.RERUN
89
+ EVENT = ReleaseType.EVENT
90
+ FORCE = ReleaseType.FORCE
91
+
92
+
67
93
  class AuditData(BaseModel):
94
+ """Audit Data model."""
95
+
96
+ model_config = ConfigDict(use_enum_values=True)
97
+
68
98
  name: str = Field(description="A workflow name.")
69
99
  release: datetime = Field(description="A release datetime.")
70
- type: str = Field(description="A running type before logging.")
100
+ type: ReleaseType = Field(
101
+ default=NORMAL, description="A running type before logging."
102
+ )
71
103
  context: DictData = Field(
72
104
  default_factory=dict,
73
105
  description="A context that receive from a workflow execution result.",
74
106
  )
107
+ run_id: str = Field(description="A running ID")
75
108
  parent_run_id: Optional[str] = Field(
76
109
  default=None, description="A parent running ID."
77
110
  )
78
- run_id: str = Field(description="A running ID")
79
111
  runs_metadata: DictData = Field(
80
112
  default_factory=dict,
81
113
  description="A runs metadata that will use to tracking this audit log.",
@@ -122,7 +154,7 @@ class BaseAudit(BaseModel, ABC):
122
154
  @abstractmethod
123
155
  def is_pointed(
124
156
  self,
125
- data: AuditData,
157
+ data: Any,
126
158
  *,
127
159
  extras: Optional[DictData] = None,
128
160
  ) -> bool:
@@ -328,21 +360,21 @@ class FileAudit(BaseAudit):
328
360
  return AuditData.model_validate(obj=json.load(f))
329
361
 
330
362
  def is_pointed(
331
- self, data: AuditData, *, extras: Optional[DictData] = None
363
+ self,
364
+ data: Any,
365
+ *,
366
+ extras: Optional[DictData] = None,
332
367
  ) -> bool:
333
368
  """Check if the release log already exists at the destination log path.
334
369
 
335
370
  Args:
336
- data: The workflow name.
371
+ data (str):
337
372
  extras: Optional extra parameters to override core config.
338
373
 
339
374
  Returns:
340
375
  bool: True if the release log exists, False otherwise.
341
376
  """
342
- # NOTE: Return False if enable writing log flag does not set.
343
- if not dynamic("enable_write_audit", extras=extras):
344
- return False
345
- return self.pointer(data).exists()
377
+ return self.pointer(AuditData.model_validate(data)).exists()
346
378
 
347
379
  def pointer(self, data: AuditData) -> Path:
348
380
  """Return release directory path generated from model data.
@@ -365,7 +397,7 @@ class FileAudit(BaseAudit):
365
397
  Self: The audit instance after saving.
366
398
  """
367
399
  audit = AuditData.model_validate(data)
368
- trace: TraceManager = get_trace(
400
+ trace: Trace = get_trace(
369
401
  audit.run_id,
370
402
  parent_run_id=audit.parent_run_id,
371
403
  extras=self.extras,
@@ -655,7 +687,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
655
687
  ValueError: If SQLite database is not properly configured.
656
688
  """
657
689
  audit = AuditData.model_validate(data)
658
- trace: TraceManager = get_trace(
690
+ trace: Trace = get_trace(
659
691
  audit.run_id,
660
692
  parent_run_id=audit.parent_run_id,
661
693
  extras=self.extras,
@@ -748,10 +780,7 @@ Audit = Annotated[
748
780
  ]
749
781
 
750
782
 
751
- def get_audit(
752
- *,
753
- extras: Optional[DictData] = None,
754
- ) -> Audit: # pragma: no cov
783
+ def get_audit(extras: Optional[DictData] = None) -> Audit: # pragma: no cov
755
784
  """Get an audit model dynamically based on the config audit path value.
756
785
 
757
786
  Args:
@@ -176,17 +176,6 @@ class YamlParser:
176
176
  """Base Load object that use to search config data by given some identity
177
177
  value like name of `Workflow` or `Crontab` templates.
178
178
 
179
- :param name: (str) A name of key of config data that read with YAML
180
- Environment object.
181
- :param path: (Path) A config path object.
182
- :param externals: (DictData) An external config data that want to add to
183
- loaded config data.
184
- :param extras: (DictDdata) An extra parameters that use to override core
185
- config values.
186
-
187
- :raise ValueError: If the data does not find on the config path with the
188
- name parameter.
189
-
190
179
  Noted:
191
180
  The config data should have `type` key for modeling validation that
192
181
  make this loader know what is config should to do pass to.
@@ -209,6 +198,23 @@ class YamlParser:
209
198
  extras: Optional[DictData] = None,
210
199
  obj: Optional[Union[object, str]] = None,
211
200
  ) -> None:
201
+ """Main constructure function.
202
+
203
+ Args:
204
+ name (str): A name of key of config data that read with YAML
205
+ Environment object.
206
+ path (Path): A config path object.
207
+ externals (DictData): An external config data that want to add to
208
+ loaded config data.
209
+ extras (DictDdata): An extra parameters that use to override core
210
+ config values.
211
+ obj (object | str): An object that want to validate from the `type`
212
+ key before keeping the config data.
213
+
214
+ Raises:
215
+ ValueError: If the data does not find on the config path with the
216
+ name parameter.
217
+ """
212
218
  self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
213
219
  self.externals: DictData = externals or {}
214
220
  self.extras: DictData = extras or {}
@@ -242,17 +248,19 @@ class YamlParser:
242
248
  """Find data with specific key and return the latest modify date data if
243
249
  this key exists multiple files.
244
250
 
245
- :param name: (str) A name of data that want to find.
246
- :param path: (Path) A config path object.
247
- :param paths: (list[Path]) A list of config path object.
248
- :param obj: (object | str) An object that want to validate matching
249
- before return.
250
- :param extras: (DictData) An extra parameter that use to override core
251
- config values.
252
- :param ignore_filename: (str) An ignore filename. Default is
253
- ``.confignore`` filename.
251
+ Args:
252
+ name (str): A name of data that want to find.
253
+ path (Path): A config path object.
254
+ paths (list[Path]): A list of config path object.
255
+ obj (object | str): An object that want to validate matching
256
+ before return.
257
+ extras (DictData): An extra parameter that use to override core
258
+ config values.
259
+ ignore_filename (str): An ignore filename. Default is
260
+ ``.confignore`` filename.
254
261
 
255
- :rtype: DictData
262
+ Returns:
263
+ DictData: A config data that was found on the searching paths.
256
264
  """
257
265
  path: Path = dynamic("conf_path", f=path, extras=extras)
258
266
  if not paths:
@@ -317,7 +325,9 @@ class YamlParser:
317
325
  ``.confignore`` filename.
318
326
  tags (list[str]): A list of tag that want to filter.
319
327
 
320
- :rtype: Iterator[tuple[str, DictData]]
328
+ Returns:
329
+ Iterator[tuple[str, DictData]]: An iterator of config data that was
330
+ found on the searching paths.
321
331
  """
322
332
  excluded: list[str] = excluded or []
323
333
  tags: list[str] = tags or []
@@ -353,8 +363,11 @@ class YamlParser:
353
363
  ):
354
364
  continue
355
365
 
356
- if (t := data.get("type")) and t == obj_type:
357
-
366
+ if (
367
+ # isinstance(data, dict) and
368
+ (t := data.get("type"))
369
+ and t == obj_type
370
+ ):
358
371
  # NOTE: Start adding file metadata.
359
372
  file_stat: os.stat_result = file.lstat()
360
373
  data["created_at"] = file_stat.st_ctime
@@ -397,6 +410,13 @@ class YamlParser:
397
410
  def filter_yaml(cls, file: Path, name: Optional[str] = None) -> DictData:
398
411
  """Read a YAML file context from an input file path and specific name.
399
412
 
413
+ Notes:
414
+ The data that will return from reading context will map with config
415
+ name if an input searching name does not pass to this function.
416
+
417
+ input: {"name": "foo", "type": "Some"}
418
+ output: {"foo": {"name": "foo", "type": "Some"}}
419
+
400
420
  Args:
401
421
  file (Path): A file path that want to extract YAML context.
402
422
  name (str): A key name that search on a YAML context.
@@ -413,7 +433,7 @@ class YamlParser:
413
433
  return (
414
434
  values[name] | {"name": name} if name in values else {}
415
435
  )
416
- return values
436
+ return {values["name"]: values} if "name" in values else values
417
437
  return {}
418
438
 
419
439
  @cached_property
@@ -166,6 +166,15 @@ class StageCancelError(StageError): ...
166
166
  class StageSkipError(StageError): ...
167
167
 
168
168
 
169
+ class StageNestedError(StageError): ...
170
+
171
+
172
+ class StageNestedCancelError(StageNestedError): ...
173
+
174
+
175
+ class StageNestedSkipError(StageNestedError): ...
176
+
177
+
169
178
  class JobError(BaseError): ...
170
179
 
171
180
 
@@ -48,7 +48,7 @@ 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, Optional, Union
51
+ from typing import Annotated, Any, Literal, Optional, Union
52
52
 
53
53
  from ddeutil.core import freeze_args
54
54
  from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
@@ -72,7 +72,7 @@ from .result import (
72
72
  )
73
73
  from .reusables import has_template, param2template
74
74
  from .stages import Stage
75
- from .traces import TraceManager, get_trace
75
+ from .traces import Trace, get_trace
76
76
  from .utils import cross_product, filter_func, gen_id
77
77
 
78
78
  MatrixFilter = list[dict[str, Union[str, int]]]
@@ -187,10 +187,8 @@ class Strategy(BaseModel):
187
187
  ),
188
188
  alias="fail-fast",
189
189
  )
190
- max_parallel: int = Field(
190
+ max_parallel: Union[int, str] = Field(
191
191
  default=1,
192
- gt=0,
193
- lt=10,
194
192
  description=(
195
193
  "The maximum number of executor thread pool that want to run "
196
194
  "parallel. This value should gather than 0 and less than 10."
@@ -427,9 +425,9 @@ class OnGCPBatch(BaseRunsOn): # pragma: no cov
427
425
  args: GCPBatchArgs = Field(alias="with")
428
426
 
429
427
 
430
- def get_discriminator_runs_on(model: dict[str, Any]) -> RunsOn:
428
+ def get_discriminator_runs_on(data: dict[str, Any]) -> RunsOn:
431
429
  """Get discriminator of the RunsOn models."""
432
- t: str = model.get("type")
430
+ t: str = data.get("type")
433
431
  return RunsOn(t) if t else LOCAL
434
432
 
435
433
 
@@ -538,13 +536,28 @@ class Job(BaseModel):
538
536
  description="An extra override config values.",
539
537
  )
540
538
 
539
+ @field_validator(
540
+ "runs_on",
541
+ mode="before",
542
+ json_schema_input_type=Union[RunsOnModel, Literal["local"]],
543
+ )
544
+ def __prepare_runs_on(cls, data: Any) -> Any:
545
+ """Prepare runs on value that was passed with string type."""
546
+ if isinstance(data, str):
547
+ if data != "local":
548
+ raise ValueError(
549
+ "runs-on that pass with str type should be `local` only"
550
+ )
551
+ return {"type": data}
552
+ return data
553
+
541
554
  @field_validator("desc", mode="after")
542
- def ___prepare_desc__(cls, value: str) -> str:
555
+ def ___prepare_desc__(cls, data: str) -> str:
543
556
  """Prepare description string that was created on a template.
544
557
 
545
558
  :rtype: str
546
559
  """
547
- return dedent(value.lstrip("\n"))
560
+ return dedent(data.lstrip("\n"))
548
561
 
549
562
  @field_validator("stages", mode="after")
550
563
  def __validate_stage_id__(cls, value: list[Stage]) -> list[Stage]:
@@ -879,7 +892,7 @@ class Job(BaseModel):
879
892
  ts: float = time.monotonic()
880
893
  parent_run_id: str = run_id
881
894
  run_id: str = gen_id((self.id or "EMPTY"), unique=True)
882
- trace: TraceManager = get_trace(
895
+ trace: Trace = get_trace(
883
896
  run_id, parent_run_id=parent_run_id, extras=self.extras
884
897
  )
885
898
  trace.info(
@@ -1016,7 +1029,7 @@ def local_execute_strategy(
1016
1029
 
1017
1030
  :rtype: tuple[Status, DictData]
1018
1031
  """
1019
- trace: TraceManager = get_trace(
1032
+ trace: Trace = get_trace(
1020
1033
  run_id, parent_run_id=parent_run_id, extras=job.extras
1021
1034
  )
1022
1035
  if strategy:
@@ -1152,7 +1165,7 @@ def local_execute(
1152
1165
  ts: float = time.monotonic()
1153
1166
  parent_run_id: StrOrNone = run_id
1154
1167
  run_id: str = gen_id((job.id or "EMPTY"), unique=True)
1155
- trace: TraceManager = get_trace(
1168
+ trace: Trace = get_trace(
1156
1169
  run_id, parent_run_id=parent_run_id, extras=job.extras
1157
1170
  )
1158
1171
  context: DictData = {"status": WAIT}
@@ -1174,11 +1187,52 @@ def local_execute(
1174
1187
 
1175
1188
  event: Event = event or Event()
1176
1189
  ls: str = "Fail-Fast" if job.strategy.fail_fast else "All-Completed"
1177
- workers: int = job.strategy.max_parallel
1190
+ workers: Union[int, str] = job.strategy.max_parallel
1191
+ if isinstance(workers, str):
1192
+ try:
1193
+ workers: int = int(
1194
+ param2template(workers, params=params, extras=job.extras)
1195
+ )
1196
+ except Exception as err:
1197
+ trace.exception(
1198
+ "[JOB]: Got the error on call param2template to "
1199
+ f"max-parallel value: {workers}"
1200
+ )
1201
+ return Result(
1202
+ run_id=run_id,
1203
+ parent_run_id=parent_run_id,
1204
+ status=FAILED,
1205
+ context=catch(
1206
+ context,
1207
+ status=FAILED,
1208
+ updated={"errors": to_dict(err)},
1209
+ ),
1210
+ info={"execution_time": time.monotonic() - ts},
1211
+ extras=job.extras,
1212
+ )
1213
+ if workers >= 10:
1214
+ err_msg: str = (
1215
+ f"The max-parallel value should not more than 10, the current value "
1216
+ f"was set: {workers}."
1217
+ )
1218
+ trace.error(f"[JOB]: {err_msg}")
1219
+ return Result(
1220
+ run_id=run_id,
1221
+ parent_run_id=parent_run_id,
1222
+ status=FAILED,
1223
+ context=catch(
1224
+ context,
1225
+ status=FAILED,
1226
+ updated={"errors": JobError(err_msg).to_dict()},
1227
+ ),
1228
+ info={"execution_time": time.monotonic() - ts},
1229
+ extras=job.extras,
1230
+ )
1231
+
1178
1232
  strategies: list[DictStr] = job.strategy.make()
1179
1233
  len_strategy: int = len(strategies)
1180
1234
  trace.info(
1181
- f"[JOB]: ... Mode {ls}: {job.id!r} with {workers} "
1235
+ f"[JOB]: Mode {ls}: {job.id!r} with {workers} "
1182
1236
  f"worker{'s' if workers > 1 else ''}."
1183
1237
  )
1184
1238
 
@@ -1295,7 +1349,7 @@ def self_hosted_execute(
1295
1349
  """
1296
1350
  parent_run_id: StrOrNone = run_id
1297
1351
  run_id: str = gen_id((job.id or "EMPTY"), unique=True)
1298
- trace: TraceManager = get_trace(
1352
+ trace: Trace = get_trace(
1299
1353
  run_id, parent_run_id=parent_run_id, extras=job.extras
1300
1354
  )
1301
1355
  context: DictData = {"status": WAIT}
@@ -1378,7 +1432,7 @@ def docker_execution(
1378
1432
  """
1379
1433
  parent_run_id: StrOrNone = run_id
1380
1434
  run_id: str = gen_id((job.id or "EMPTY"), unique=True)
1381
- trace: TraceManager = get_trace(
1435
+ trace: Trace = get_trace(
1382
1436
  run_id, parent_run_id=parent_run_id, extras=job.extras
1383
1437
  )
1384
1438
  context: DictData = {"status": WAIT}