ddeutil-workflow 0.0.41__py3-none-any.whl → 0.0.42__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.
@@ -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
- # [x] Use config
6
+ # [x] Use dynamic config
7
7
  """Stage Model that use for getting stage data template from the Job Model.
8
8
  The stage handle the minimize task that run in some thread (same thread at
9
9
  its job owner) that mean it is the lowest executor of a workflow that can
@@ -50,7 +50,7 @@ from pydantic.functional_validators import model_validator
50
50
  from typing_extensions import Self
51
51
 
52
52
  from .__types import DictData, DictStr, TupleStr
53
- from .conf import config
53
+ from .conf import dynamic
54
54
  from .exceptions import StageException, to_dict
55
55
  from .result import Result, Status
56
56
  from .reusables import TagFunc, extract_call, not_in_template, param2template
@@ -67,6 +67,7 @@ __all__: TupleStr = (
67
67
  "TriggerStage",
68
68
  "ForEachStage",
69
69
  "ParallelStage",
70
+ "RaiseStage",
70
71
  "Stage",
71
72
  )
72
73
 
@@ -96,7 +97,7 @@ class BaseStage(BaseModel, ABC):
96
97
  )
97
98
  extras: DictData = Field(
98
99
  default_factory=dict,
99
- description="An extra override values.",
100
+ description="An extra override config values.",
100
101
  )
101
102
 
102
103
  @property
@@ -151,15 +152,6 @@ class BaseStage(BaseModel, ABC):
151
152
  """
152
153
  raise NotImplementedError("Stage should implement `execute` method.")
153
154
 
154
- async def axecute(
155
- self,
156
- params: DictData,
157
- *,
158
- result: Result | None = None,
159
- event: Event | None,
160
- ) -> Result: # pragma: no cov
161
- ...
162
-
163
155
  def handler_execute(
164
156
  self,
165
157
  params: DictData,
@@ -167,7 +159,7 @@ class BaseStage(BaseModel, ABC):
167
159
  run_id: str | None = None,
168
160
  parent_run_id: str | None = None,
169
161
  result: Result | None = None,
170
- raise_error: bool = False,
162
+ raise_error: bool | None = None,
171
163
  to: DictData | None = None,
172
164
  event: Event | None = None,
173
165
  ) -> Result:
@@ -223,7 +215,7 @@ class BaseStage(BaseModel, ABC):
223
215
  except Exception as err:
224
216
  result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
225
217
 
226
- if raise_error or config.stage_raise_error:
218
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
227
219
  if isinstance(err, StageException):
228
220
  raise
229
221
 
@@ -268,13 +260,17 @@ class BaseStage(BaseModel, ABC):
268
260
  if "stages" not in to:
269
261
  to["stages"] = {}
270
262
 
271
- if self.id is None and not config.stage_default_id:
263
+ if self.id is None and not dynamic(
264
+ "stage_default_id", extras=self.extras
265
+ ):
272
266
  return to
273
267
 
274
268
  _id: str = (
275
- param2template(self.id, params=to)
269
+ param2template(self.id, params=to, extras=self.extras)
276
270
  if self.id
277
- else gen_id(param2template(self.name, params=to))
271
+ else gen_id(
272
+ param2template(self.name, params=to, extras=self.extras)
273
+ )
278
274
  )
279
275
 
280
276
  errors: DictData = (
@@ -312,7 +308,9 @@ class BaseStage(BaseModel, ABC):
312
308
  # should use the `re` module to validate eval-string before
313
309
  # running.
314
310
  rs: bool = eval(
315
- param2template(self.condition, params), globals() | params, {}
311
+ param2template(self.condition, params, extras=self.extras),
312
+ globals() | params,
313
+ {},
316
314
  )
317
315
  if not isinstance(rs, bool):
318
316
  raise TypeError("Return type of condition does not be boolean")
@@ -321,7 +319,102 @@ class BaseStage(BaseModel, ABC):
321
319
  raise StageException(f"{err.__class__.__name__}: {err}") from err
322
320
 
323
321
 
324
- class EmptyStage(BaseStage):
322
+ class BaseAsyncStage(BaseStage):
323
+
324
+ @abstractmethod
325
+ def execute(
326
+ self,
327
+ params: DictData,
328
+ *,
329
+ result: Result | None = None,
330
+ event: Event | None = None,
331
+ ) -> Result: ...
332
+
333
+ @abstractmethod
334
+ async def axecute(
335
+ self,
336
+ params: DictData,
337
+ *,
338
+ result: Result | None = None,
339
+ event: Event | None = None,
340
+ ) -> Result:
341
+ """Async execution method for this Empty stage that only logging out to
342
+ stdout.
343
+
344
+ :param params: (DictData) A context data that want to add output result.
345
+ But this stage does not pass any output.
346
+ :param result: (Result) A result object for keeping context and status
347
+ data.
348
+ :param event: (Event) An event manager that use to track parent execute
349
+ was not force stopped.
350
+
351
+ :rtype: Result
352
+ """
353
+ raise NotImplementedError(
354
+ "Async Stage should implement `axecute` method."
355
+ )
356
+
357
+ async def handler_axecute(
358
+ self,
359
+ params: DictData,
360
+ *,
361
+ run_id: str | None = None,
362
+ parent_run_id: str | None = None,
363
+ result: Result | None = None,
364
+ raise_error: bool | None = None,
365
+ to: DictData | None = None,
366
+ event: Event | None = None,
367
+ ) -> Result:
368
+ """Async Handler stage execution result from the stage `execute` method.
369
+
370
+ :param params: (DictData) A parameterize value data that use in this
371
+ stage execution.
372
+ :param run_id: (str) A running stage ID for this execution.
373
+ :param parent_run_id: (str) A parent workflow running ID for this
374
+ execution.
375
+ :param result: (Result) A result object for keeping context and status
376
+ data before execution.
377
+ :param raise_error: (bool) A flag that all this method raise error
378
+ :param to: (DictData) A target object for auto set the return output
379
+ after execution.
380
+ :param event: (Event) An event manager that pass to the stage execution.
381
+
382
+ :rtype: Result
383
+ """
384
+ result: Result = Result.construct_with_rs_or_id(
385
+ result,
386
+ run_id=run_id,
387
+ parent_run_id=parent_run_id,
388
+ id_logic=self.iden,
389
+ )
390
+
391
+ try:
392
+ rs: Result = await self.axecute(params, result=result, event=event)
393
+ if to is not None:
394
+ return self.set_outputs(rs.context, to=to)
395
+ return rs
396
+ except Exception as err:
397
+ await result.trace.aerror(
398
+ f"[STAGE]: {err.__class__.__name__}: {err}"
399
+ )
400
+
401
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
402
+ if isinstance(err, StageException):
403
+ raise
404
+
405
+ raise StageException(
406
+ f"{self.__class__.__name__}: \n\t"
407
+ f"{err.__class__.__name__}: {err}"
408
+ ) from None
409
+
410
+ errors: DictData = {"errors": to_dict(err)}
411
+ if to is not None:
412
+ return self.set_outputs(errors, to=to)
413
+
414
+ return result.catch(status=Status.FAILED, context=errors)
415
+
416
+
417
+ class EmptyStage(BaseAsyncStage):
325
418
  """Empty stage that do nothing (context equal empty stage) and logging the
326
419
  name of stage only to stdout.
327
420
 
@@ -372,7 +465,7 @@ class EmptyStage(BaseStage):
372
465
 
373
466
  result.trace.info(
374
467
  f"[STAGE]: Empty-Execute: {self.name!r}: "
375
- f"( {param2template(self.echo, params=params) or '...'} )"
468
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
376
469
  )
377
470
  if self.sleep > 0:
378
471
  if self.sleep > 5:
@@ -381,14 +474,13 @@ class EmptyStage(BaseStage):
381
474
 
382
475
  return result.catch(status=Status.SUCCESS)
383
476
 
384
- # TODO: Draft async execute method for the perf improvement.
385
477
  async def axecute(
386
478
  self,
387
479
  params: DictData,
388
480
  *,
389
481
  result: Result | None = None,
390
- event: Event | None,
391
- ) -> Result: # pragma: no cov
482
+ event: Event | None = None,
483
+ ) -> Result:
392
484
  """Async execution method for this Empty stage that only logging out to
393
485
  stdout.
394
486
 
@@ -406,14 +498,16 @@ class EmptyStage(BaseStage):
406
498
  run_id=gen_id(self.name + (self.id or ""), unique=True)
407
499
  )
408
500
 
409
- result.trace.info(
501
+ await result.trace.ainfo(
410
502
  f"[STAGE]: Empty-Execute: {self.name!r}: "
411
- f"( {param2template(self.echo, params=params) or '...'} )"
503
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
412
504
  )
413
505
 
414
506
  if self.sleep > 0:
415
507
  if self.sleep > 5:
416
- result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
508
+ await result.trace.ainfo(
509
+ f"[STAGE]: ... sleep ({self.sleep} seconds)"
510
+ )
417
511
  await asyncio.sleep(self.sleep)
418
512
 
419
513
  return result.catch(status=Status.SUCCESS)
@@ -508,12 +602,14 @@ class BashStage(BaseStage):
508
602
  run_id=gen_id(self.name + (self.id or ""), unique=True)
509
603
  )
510
604
 
511
- bash: str = param2template(dedent(self.bash), params)
605
+ bash: str = param2template(
606
+ dedent(self.bash), params, extras=self.extras
607
+ )
512
608
 
513
609
  result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
514
610
  with self.create_sh_file(
515
611
  bash=bash,
516
- env=param2template(self.env, params),
612
+ env=param2template(self.env, params, extras=self.extras),
517
613
  run_id=result.run_id,
518
614
  ) as sh:
519
615
  result.trace.debug(f"... Start create `{sh[1]}` file.")
@@ -635,7 +731,7 @@ class PyStage(BaseStage):
635
731
  gb: DictData = (
636
732
  globals()
637
733
  | params
638
- | param2template(self.vars, params)
734
+ | param2template(self.vars, params, extras=self.extras)
639
735
  | {"result": result}
640
736
  )
641
737
 
@@ -648,7 +744,9 @@ class PyStage(BaseStage):
648
744
 
649
745
  # WARNING: The exec build-in function is very dangerous. So, it
650
746
  # should use the re module to validate exec-string before running.
651
- exec(param2template(dedent(self.run), params), gb, lc)
747
+ exec(
748
+ param2template(dedent(self.run), params, extras=self.extras), gb, lc
749
+ )
652
750
 
653
751
  return result.catch(
654
752
  status=Status.SUCCESS, context={"locals": lc, "globals": gb}
@@ -716,11 +814,16 @@ class CallStage(BaseStage):
716
814
  run_id=gen_id(self.name + (self.id or ""), unique=True)
717
815
  )
718
816
 
719
- t_func: TagFunc = extract_call(param2template(self.uses, params))()
817
+ t_func: TagFunc = extract_call(
818
+ param2template(self.uses, params, extras=self.extras),
819
+ registries=self.extras.get("regis_call"),
820
+ )()
720
821
 
721
822
  # VALIDATE: check input task caller parameters that exists before
722
823
  # calling.
723
- args: DictData = {"result": result} | param2template(self.args, params)
824
+ args: DictData = {"result": result} | param2template(
825
+ self.args, params, extras=self.extras
826
+ )
724
827
  ips = inspect.signature(t_func)
725
828
  necessary_params: list[str] = [
726
829
  k
@@ -754,10 +857,12 @@ class CallStage(BaseStage):
754
857
  if inspect.iscoroutinefunction(t_func): # pragma: no cov
755
858
  loop = asyncio.get_event_loop()
756
859
  rs: DictData = loop.run_until_complete(
757
- t_func(**param2template(args, params))
860
+ t_func(**param2template(args, params, extras=self.extras))
758
861
  )
759
862
  else:
760
- rs: DictData = t_func(**param2template(args, params))
863
+ rs: DictData = t_func(
864
+ **param2template(args, params, extras=self.extras)
865
+ )
761
866
 
762
867
  # VALIDATE:
763
868
  # Check the result type from call function, it should be dict.
@@ -819,15 +924,16 @@ class TriggerStage(BaseStage):
819
924
  )
820
925
 
821
926
  # NOTE: Loading workflow object from trigger name.
822
- _trigger: str = param2template(self.trigger, params=params)
927
+ _trigger: str = param2template(self.trigger, params, extras=self.extras)
823
928
 
824
929
  # NOTE: Set running workflow ID from running stage ID to external
825
930
  # params on Loader object.
826
- workflow: Workflow = Workflow.from_loader(name=_trigger)
931
+ workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
827
932
  result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
828
933
  return workflow.execute(
829
- params=param2template(self.params, params),
934
+ params=param2template(self.params, params, extras=self.extras),
830
935
  result=result,
936
+ event=event,
831
937
  )
832
938
 
833
939
 
@@ -931,6 +1037,9 @@ class ParallelStage(BaseStage): # pragma: no cov
931
1037
  run_id=gen_id(self.name + (self.id or ""), unique=True)
932
1038
  )
933
1039
 
1040
+ result.trace.info(
1041
+ f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
1042
+ )
934
1043
  rs: DictData = {"parallel": {}}
935
1044
  status = Status.SUCCESS
936
1045
  with ThreadPoolExecutor(
@@ -981,7 +1090,7 @@ class ForEachStage(BaseStage):
981
1090
  ... }
982
1091
  """
983
1092
 
984
- foreach: Union[list[str], list[int]] = Field(
1093
+ foreach: Union[list[str], list[int], str] = Field(
985
1094
  description=(
986
1095
  "A items for passing to each stages via ${{ item }} template."
987
1096
  ),
@@ -1023,10 +1132,21 @@ class ForEachStage(BaseStage):
1023
1132
  run_id=gen_id(self.name + (self.id or ""), unique=True)
1024
1133
  )
1025
1134
 
1026
- rs: DictData = {"items": self.foreach, "foreach": {}}
1135
+ foreach: Union[list[str], list[int]] = (
1136
+ param2template(self.foreach, params, extras=self.extras)
1137
+ if isinstance(self.foreach, str)
1138
+ else self.foreach
1139
+ )
1140
+ if not isinstance(foreach, list):
1141
+ raise StageException(
1142
+ f"Foreach does not support foreach value: {foreach!r}"
1143
+ )
1144
+
1145
+ result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
1146
+ rs: DictData = {"items": foreach, "foreach": {}}
1027
1147
  status = Status.SUCCESS
1028
1148
  # TODO: Implement concurrent more than 1.
1029
- for item in self.foreach:
1149
+ for item in foreach:
1030
1150
  result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
1031
1151
  params["item"] = item
1032
1152
  context = {"stages": {}}
@@ -1064,10 +1184,24 @@ class ForEachStage(BaseStage):
1064
1184
 
1065
1185
  # TODO: Not implement this stages yet
1066
1186
  class UntilStage(BaseStage): # pragma: no cov
1067
- """Until execution stage."""
1187
+ """Until execution stage.
1188
+
1189
+ Data Validate:
1190
+ >>> stage = {
1191
+ ... "name": "Until stage execution",
1192
+ ... "item": 1,
1193
+ ... "until": "${{ item }} > 3"
1194
+ ... "stages": [
1195
+ ... {
1196
+ ... "name": "Start increase item value.",
1197
+ ... "run": "item = ${{ item }}\\nitem += 1\\n"
1198
+ ... },
1199
+ ... ],
1200
+ ... }
1201
+ """
1068
1202
 
1069
- until: str = Field(description="A until condition.")
1070
1203
  item: Union[str, int, bool] = Field(description="An initial value.")
1204
+ until: str = Field(description="A until condition.")
1071
1205
  stages: list[Stage] = Field(
1072
1206
  default_factory=list,
1073
1207
  description=(
@@ -1090,8 +1224,14 @@ class UntilStage(BaseStage): # pragma: no cov
1090
1224
 
1091
1225
 
1092
1226
  # TODO: Not implement this stages yet
1093
- class IfStage(BaseStage): # pragma: no cov
1094
- """If execution stage.
1227
+ class Match(BaseModel):
1228
+ case: Union[str, int]
1229
+ stage: Stage
1230
+
1231
+
1232
+ # TODO: Not implement this stages yet
1233
+ class CaseStage(BaseStage): # pragma: no cov
1234
+ """Case execution stage.
1095
1235
 
1096
1236
  Data Validate:
1097
1237
  >>> stage = {
@@ -1125,7 +1265,7 @@ class IfStage(BaseStage): # pragma: no cov
1125
1265
  """
1126
1266
 
1127
1267
  case: str = Field(description="A case condition for routing.")
1128
- match: list[dict[str, Union[str, Stage]]]
1268
+ match: list[Match]
1129
1269
 
1130
1270
  def execute(
1131
1271
  self,
@@ -1133,7 +1273,60 @@ class IfStage(BaseStage): # pragma: no cov
1133
1273
  *,
1134
1274
  result: Result | None = None,
1135
1275
  event: Event | None = None,
1136
- ) -> Result: ...
1276
+ ) -> Result:
1277
+ """Execute case-match condition that pass to the case field.
1278
+
1279
+ :param params: A parameter that want to pass before run any statement.
1280
+ :param result: (Result) A result object for keeping context and status
1281
+ data.
1282
+ :param event: (Event) An event manager that use to track parent execute
1283
+ was not force stopped.
1284
+
1285
+ :rtype: Result
1286
+ """
1287
+ if result is None: # pragma: no cov
1288
+ result: Result = Result(
1289
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1290
+ )
1291
+ status = Status.SUCCESS
1292
+ _case = param2template(self.case, params, extras=self.extras)
1293
+ _else = None
1294
+ context = {}
1295
+ for match in self.match:
1296
+ if (c := match.case) != "_":
1297
+ _condition = param2template(c, params, extras=self.extras)
1298
+ else:
1299
+ _else = match
1300
+ continue
1301
+
1302
+ if match == _condition:
1303
+ stage: Stage = match.stage
1304
+ try:
1305
+ stage.set_outputs(
1306
+ stage.handler_execute(
1307
+ params=params,
1308
+ run_id=result.run_id,
1309
+ parent_run_id=result.parent_run_id,
1310
+ ).context,
1311
+ to=context,
1312
+ )
1313
+ except StageException as err: # pragma: no cov
1314
+ status = Status.FAILED
1315
+ result.trace.error(
1316
+ f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
1317
+ f"\n\t{err}"
1318
+ )
1319
+ context.update(
1320
+ {
1321
+ "errors": {
1322
+ "class": err,
1323
+ "name": err.__class__.__name__,
1324
+ "message": f"{err.__class__.__name__}: {err}",
1325
+ },
1326
+ },
1327
+ )
1328
+
1329
+ return result.catch(status=status, context=context)
1137
1330
 
1138
1331
 
1139
1332
  class RaiseStage(BaseStage): # pragma: no cov
@@ -1165,13 +1358,14 @@ class RaiseStage(BaseStage): # pragma: no cov
1165
1358
  result: Result = Result(
1166
1359
  run_id=gen_id(self.name + (self.id or ""), unique=True)
1167
1360
  )
1168
-
1169
- result.trace.error(f"[STAGE]: ... raise ( {self.message} )")
1361
+ result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
1170
1362
  raise StageException(self.message)
1171
1363
 
1172
1364
 
1173
1365
  # TODO: Not implement this stages yet
1174
1366
  class HookStage(BaseStage): # pragma: no cov
1367
+ """Hook stage execution."""
1368
+
1175
1369
  hook: str
1176
1370
  args: DictData
1177
1371
  callback: str
@@ -1187,7 +1381,20 @@ class HookStage(BaseStage): # pragma: no cov
1187
1381
 
1188
1382
  # TODO: Not implement this stages yet
1189
1383
  class DockerStage(BaseStage): # pragma: no cov
1190
- """Docker container stage execution."""
1384
+ """Docker container stage execution.
1385
+
1386
+ Data Validate:
1387
+ >>> stage = {
1388
+ ... "name": "Docker stage execution",
1389
+ ... "image": "image-name.pkg.com",
1390
+ ... "env": {
1391
+ ... "ENV": "dev",
1392
+ ... },
1393
+ ... "volume": {
1394
+ ... "secrets": "/secrets",
1395
+ ... },
1396
+ ... }
1397
+ """
1191
1398
 
1192
1399
  image: str = Field(
1193
1400
  description="A Docker image url with tag that want to run.",
@@ -1224,18 +1431,6 @@ class VirtualPyStage(PyStage): # pragma: no cov
1224
1431
  return super().execute(params, result=result)
1225
1432
 
1226
1433
 
1227
- # TODO: Not implement this stages yet
1228
- class SensorStage(BaseStage): # pragma: no cov
1229
-
1230
- def execute(
1231
- self,
1232
- params: DictData,
1233
- *,
1234
- result: Result | None = None,
1235
- event: Event | None = None,
1236
- ) -> Result: ...
1237
-
1238
-
1239
1434
  # NOTE:
1240
1435
  # An order of parsing stage model on the Job model with ``stages`` field.
1241
1436
  # From the current build-in stages, they do not have stage that have the same
@@ -1243,7 +1438,6 @@ class SensorStage(BaseStage): # pragma: no cov
1243
1438
  #
1244
1439
  Stage = Annotated[
1245
1440
  Union[
1246
- EmptyStage,
1247
1441
  BashStage,
1248
1442
  CallStage,
1249
1443
  TriggerStage,
@@ -1251,6 +1445,7 @@ Stage = Annotated[
1251
1445
  ParallelStage,
1252
1446
  PyStage,
1253
1447
  RaiseStage,
1448
+ EmptyStage,
1254
1449
  ],
1255
1450
  Field(union_mode="smart"),
1256
1451
  ]
ddeutil/workflow/utils.py CHANGED
@@ -3,7 +3,6 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- # [ ] Use config
7
6
  """Utility function model."""
8
7
  from __future__ import annotations
9
8