ddeutil-workflow 0.0.81__py3-none-any.whl → 0.0.82__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.
ddeutil/workflow/conf.py CHANGED
@@ -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
 
ddeutil/workflow/job.py CHANGED
@@ -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}
@@ -21,12 +21,12 @@ from __future__ import annotations
21
21
 
22
22
  from dataclasses import field
23
23
  from enum import Enum
24
- from typing import Optional, Union
24
+ from typing import Optional, TypedDict, Union
25
25
 
26
26
  from pydantic import ConfigDict
27
27
  from pydantic.dataclasses import dataclass
28
28
  from pydantic.functional_validators import model_validator
29
- from typing_extensions import Self
29
+ from typing_extensions import NotRequired, Self
30
30
 
31
31
  from . import (
32
32
  JobCancelError,
@@ -34,13 +34,16 @@ from . import (
34
34
  JobSkipError,
35
35
  StageCancelError,
36
36
  StageError,
37
+ StageNestedCancelError,
38
+ StageNestedError,
39
+ StageNestedSkipError,
37
40
  StageSkipError,
38
41
  WorkflowCancelError,
39
42
  WorkflowError,
40
43
  )
41
44
  from .__types import DictData
42
- from .audits import TraceManager, get_trace
43
- from .errors import ResultError
45
+ from .audits import Trace, get_trace
46
+ from .errors import ErrorData, ResultError
44
47
  from .utils import default_gen_id
45
48
 
46
49
 
@@ -140,6 +143,9 @@ def get_status_from_error(
140
143
  StageError,
141
144
  StageCancelError,
142
145
  StageSkipError,
146
+ StageNestedCancelError,
147
+ StageNestedError,
148
+ StageNestedSkipError,
143
149
  JobError,
144
150
  JobCancelError,
145
151
  JobSkipError,
@@ -157,10 +163,16 @@ def get_status_from_error(
157
163
  Returns:
158
164
  Status: The status from the specific exception class.
159
165
  """
160
- if isinstance(error, (StageSkipError, JobSkipError)):
166
+ if isinstance(error, (StageNestedSkipError, StageSkipError, JobSkipError)):
161
167
  return SKIP
162
168
  elif isinstance(
163
- error, (StageCancelError, JobCancelError, WorkflowCancelError)
169
+ error,
170
+ (
171
+ StageNestedCancelError,
172
+ StageCancelError,
173
+ JobCancelError,
174
+ WorkflowCancelError,
175
+ ),
164
176
  ):
165
177
  return CANCEL
166
178
  return FAILED
@@ -188,9 +200,7 @@ class Result:
188
200
  info: DictData = field(default_factory=dict)
189
201
  run_id: str = field(default_factory=default_gen_id)
190
202
  parent_run_id: Optional[str] = field(default=None)
191
- trace: Optional[TraceManager] = field(
192
- default=None, compare=False, repr=False
193
- )
203
+ trace: Optional[Trace] = field(default=None, compare=False, repr=False)
194
204
 
195
205
  @model_validator(mode="after")
196
206
  def __prepare_trace(self) -> Self:
@@ -199,7 +209,7 @@ class Result:
199
209
  :rtype: Self
200
210
  """
201
211
  if self.trace is None: # pragma: no cov
202
- self.trace: TraceManager = get_trace(
212
+ self.trace: Trace = get_trace(
203
213
  self.run_id,
204
214
  parent_run_id=self.parent_run_id,
205
215
  extras=self.extras,
@@ -208,7 +218,7 @@ class Result:
208
218
  return self
209
219
 
210
220
  @classmethod
211
- def from_trace(cls, trace: TraceManager):
221
+ def from_trace(cls, trace: Trace):
212
222
  """Construct the result model from trace for clean code objective."""
213
223
  return cls(
214
224
  run_id=trace.run_id,
@@ -274,6 +284,9 @@ def catch(
274
284
  context: A context data that want to be the current context.
275
285
  status: A status enum object.
276
286
  updated: A updated data that will update to the current context.
287
+
288
+ Returns:
289
+ DictData: A catch context data.
277
290
  """
278
291
  context.update(updated or {})
279
292
  context["status"] = Status(status) if isinstance(status, int) else status
@@ -291,3 +304,12 @@ def catch(
291
304
  else:
292
305
  raise ResultError(f"The key {k!r} does not exists on context data.")
293
306
  return context
307
+
308
+
309
+ class Context(TypedDict):
310
+ """Context dict typed."""
311
+
312
+ status: Status
313
+ context: NotRequired[DictData]
314
+ errors: NotRequired[Union[list[ErrorData], ErrorData]]
315
+ info: NotRequired[DictData]