ddeutil-workflow 0.0.48__py3-none-any.whl → 0.0.49__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.
@@ -43,7 +43,7 @@ from pathlib import Path
43
43
  from subprocess import CompletedProcess
44
44
  from textwrap import dedent
45
45
  from threading import Event
46
- from typing import Annotated, Any, Optional, Union, get_type_hints
46
+ from typing import Annotated, Any, Optional, TypeVar, Union, get_type_hints
47
47
 
48
48
  from pydantic import BaseModel, Field
49
49
  from pydantic.functional_validators import model_validator
@@ -59,17 +59,7 @@ from .utils import (
59
59
  make_exec,
60
60
  )
61
61
 
62
- __all__: TupleStr = (
63
- "EmptyStage",
64
- "BashStage",
65
- "PyStage",
66
- "CallStage",
67
- "TriggerStage",
68
- "ForEachStage",
69
- "ParallelStage",
70
- "RaiseStage",
71
- "Stage",
72
- )
62
+ T = TypeVar("T")
73
63
 
74
64
 
75
65
  class BaseStage(BaseModel, ABC):
@@ -284,6 +274,25 @@ class BaseStage(BaseModel, ABC):
284
274
  to["stages"][_id] = {"outputs": output, **skipping, **errors}
285
275
  return to
286
276
 
277
+ def get_outputs(self, outputs: DictData) -> DictData:
278
+ """Get the outputs from stages data.
279
+
280
+ :rtype: DictData
281
+ """
282
+ if self.id is None and not dynamic(
283
+ "stage_default_id", extras=self.extras
284
+ ):
285
+ return {}
286
+
287
+ _id: str = (
288
+ param2template(self.id, params=outputs, extras=self.extras)
289
+ if self.id
290
+ else gen_id(
291
+ param2template(self.name, params=outputs, extras=self.extras)
292
+ )
293
+ )
294
+ return outputs.get("stages", {}).get(_id, {})
295
+
287
296
  def is_skipped(self, params: DictData | None = None) -> bool:
288
297
  """Return true if condition of this stage do not correct. This process
289
298
  use build-in eval function to execute the if-condition.
@@ -819,7 +828,7 @@ class CallStage(BaseStage):
819
828
  has_keyword: bool = False
820
829
  call_func: TagFunc = extract_call(
821
830
  param2template(self.uses, params, extras=self.extras),
822
- registries=self.extras.get("regis_call"),
831
+ registries=self.extras.get("registry_caller"),
823
832
  )()
824
833
 
825
834
  # VALIDATE: check input task caller parameters that exists before
@@ -1146,13 +1155,59 @@ class ForEachStage(BaseStage):
1146
1155
  )
1147
1156
  concurrent: int = Field(
1148
1157
  default=1,
1149
- gt=0,
1158
+ ge=1,
1159
+ lt=10,
1150
1160
  description=(
1151
1161
  "A concurrent value allow to run each item at the same time. It "
1152
1162
  "will be sequential mode if this value equal 1."
1153
1163
  ),
1154
1164
  )
1155
1165
 
1166
+ def execute_item(
1167
+ self,
1168
+ item: Union[str, int],
1169
+ params: DictData,
1170
+ context: DictData,
1171
+ result: Result,
1172
+ ) -> tuple[Status, DictData]:
1173
+ """Execute foreach item from list of item.
1174
+
1175
+ :param item: (str | int) An item that want to execution.
1176
+ :param params: (DictData) A parameter that want to pass to stage
1177
+ execution.
1178
+ :param context: (DictData)
1179
+ :param result: (Result)
1180
+
1181
+ :rtype: tuple[Status, DictData]
1182
+ """
1183
+ result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
1184
+ params["item"] = item
1185
+ to: DictData = {"item": item, "stages": {}}
1186
+ status: Status = SUCCESS
1187
+ for stage in self.stages:
1188
+
1189
+ if self.extras:
1190
+ stage.extras = self.extras
1191
+
1192
+ try:
1193
+ stage.set_outputs(
1194
+ stage.handler_execute(
1195
+ params=params,
1196
+ run_id=result.run_id,
1197
+ parent_run_id=result.parent_run_id,
1198
+ ).context,
1199
+ to=to,
1200
+ )
1201
+ except StageException as e: # pragma: no cov
1202
+ status = FAILED
1203
+ result.trace.error(
1204
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1205
+ )
1206
+ to.update({"errors": e.to_dict()})
1207
+
1208
+ context["foreach"][item] = to
1209
+ return status, context
1210
+
1156
1211
  def execute(
1157
1212
  self,
1158
1213
  params: DictData,
@@ -1186,41 +1241,26 @@ class ForEachStage(BaseStage):
1186
1241
  )
1187
1242
 
1188
1243
  result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
1189
- rs: DictData = {"items": foreach, "foreach": {}}
1190
- status: Status = SUCCESS
1191
- # TODO: Implement concurrent more than 1.
1192
- for item in foreach:
1193
- result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
1194
- params["item"] = item
1195
- context = {"stages": {}}
1196
-
1197
- for stage in self.stages:
1198
-
1199
- if self.extras:
1200
- stage.extras = self.extras
1201
-
1202
- try:
1203
- stage.set_outputs(
1204
- stage.handler_execute(
1205
- params=params,
1206
- run_id=result.run_id,
1207
- parent_run_id=result.parent_run_id,
1208
- ).context,
1209
- to=context,
1210
- )
1211
- except StageException as e: # pragma: no cov
1212
- status = FAILED
1213
- result.trace.error(
1214
- f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1215
- )
1216
- context.update({"errors": e.to_dict()})
1217
-
1218
- rs["foreach"][item] = context
1244
+ context: DictData = {"items": foreach, "foreach": {}}
1245
+ statuses: list[Union[SUCCESS, FAILED]] = []
1246
+ with ThreadPoolExecutor(max_workers=self.concurrent) as executor:
1247
+ futures: list[Future] = [
1248
+ executor.submit(
1249
+ self.execute_item,
1250
+ item=item,
1251
+ params=params.copy(),
1252
+ context=context,
1253
+ result=result,
1254
+ )
1255
+ for item in foreach
1256
+ ]
1257
+ for future in as_completed(futures):
1258
+ status, context = future.result()
1259
+ statuses.append(status)
1219
1260
 
1220
- return result.catch(status=status, context=rs)
1261
+ return result.catch(status=max(statuses), context=context)
1221
1262
 
1222
1263
 
1223
- # TODO: Not implement this stages yet
1224
1264
  class UntilStage(BaseStage): # pragma: no cov
1225
1265
  """Until execution stage.
1226
1266
 
@@ -1247,27 +1287,144 @@ class UntilStage(BaseStage): # pragma: no cov
1247
1287
  "correct."
1248
1288
  ),
1249
1289
  )
1250
- max_until_not_change: int = Field(
1251
- default=3,
1252
- description="The maximum value of loop if condition not change.",
1290
+ max_until_loop: int = Field(
1291
+ default=10,
1292
+ ge=1,
1293
+ lt=100,
1294
+ description="The maximum value of loop for this until stage.",
1253
1295
  )
1254
1296
 
1297
+ def execute_item(
1298
+ self,
1299
+ item: T,
1300
+ loop: int,
1301
+ params: DictData,
1302
+ context: DictData,
1303
+ result: Result,
1304
+ ) -> tuple[Status, DictData, T]:
1305
+ """Execute until item set item by some stage or by default loop
1306
+ variable.
1307
+
1308
+ :param item: (T) An item that want to execution.
1309
+ :param loop: (int) A number of loop.
1310
+ :param params: (DictData) A parameter that want to pass to stage
1311
+ execution.
1312
+ :param context: (DictData)
1313
+ :param result: (Result)
1314
+
1315
+ :rtype: tuple[Status, DictData, T]
1316
+ """
1317
+ result.trace.debug(f"[STAGE]: Execute until item: {item!r}")
1318
+ params["item"] = item
1319
+ to: DictData = {"item": item, "stages": {}}
1320
+ status: Status = SUCCESS
1321
+ next_item: T = None
1322
+ for stage in self.stages:
1323
+
1324
+ if self.extras:
1325
+ stage.extras = self.extras
1326
+
1327
+ try:
1328
+ stage.set_outputs(
1329
+ stage.handler_execute(
1330
+ params=params,
1331
+ run_id=result.run_id,
1332
+ parent_run_id=result.parent_run_id,
1333
+ ).context,
1334
+ to=to,
1335
+ )
1336
+ if "item" in (
1337
+ outputs := stage.get_outputs(to).get("outputs", {})
1338
+ ):
1339
+ next_item = outputs["item"]
1340
+ except StageException as e:
1341
+ status = FAILED
1342
+ result.trace.error(
1343
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1344
+ )
1345
+ to.update({"errors": e.to_dict()})
1346
+
1347
+ context["until"][loop] = to
1348
+ return status, context, next_item
1349
+
1255
1350
  def execute(
1256
1351
  self,
1257
1352
  params: DictData,
1258
1353
  *,
1259
1354
  result: Result | None = None,
1260
1355
  event: Event | None = None,
1261
- ) -> Result: ...
1356
+ ) -> Result:
1357
+ """Execute the stages that pass item from until condition field and
1358
+ setter step.
1359
+
1360
+ :param params: A parameter that want to pass before run any statement.
1361
+ :param result: (Result) A result object for keeping context and status
1362
+ data.
1363
+ :param event: (Event) An event manager that use to track parent execute
1364
+ was not force stopped.
1365
+
1366
+ :rtype: Result
1367
+ """
1368
+ if result is None: # pragma: no cov
1369
+ result: Result = Result(
1370
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1371
+ )
1372
+
1373
+ item: Union[str, int, bool] = param2template(
1374
+ self.item, params, extras=self.extras
1375
+ )
1376
+ result.trace.info(f"[STAGE]: Until-Execution: {self.until}")
1377
+ track: bool = True
1378
+ exceed_loop: bool = False
1379
+ loop: int = 1
1380
+ context: DictData = {"until": {}}
1381
+ statuses: list[Union[SUCCESS, FAILED]] = []
1382
+ while track and not (exceed_loop := loop >= self.max_until_loop):
1383
+ status, context, item = self.execute_item(
1384
+ item=item,
1385
+ loop=loop,
1386
+ params=params.copy(),
1387
+ context=context,
1388
+ result=result,
1389
+ )
1390
+
1391
+ loop += 1
1392
+ if item is None:
1393
+ result.trace.warning(
1394
+ "... Does not have set item stage. It will use loop by "
1395
+ "default."
1396
+ )
1397
+ item = loop
1398
+
1399
+ next_track: bool = eval(
1400
+ param2template(
1401
+ self.until, params | {"item": item}, extras=self.extras
1402
+ ),
1403
+ globals() | params | {"item": item},
1404
+ {},
1405
+ )
1406
+ if not isinstance(next_track, bool):
1407
+ raise TypeError(
1408
+ "Return type of until condition does not be boolean, it"
1409
+ f"return: {next_track!r}"
1410
+ )
1411
+ track = not next_track
1412
+ statuses.append(status)
1413
+
1414
+ if exceed_loop:
1415
+ raise StageException(
1416
+ f"The until loop was exceed {self.max_until_loop} loops"
1417
+ )
1418
+ return result.catch(status=max(statuses), context=context)
1262
1419
 
1263
1420
 
1264
- # TODO: Not implement this stages yet
1265
1421
  class Match(BaseModel):
1422
+ """Match model for the Case Stage."""
1423
+
1266
1424
  case: Union[str, int]
1267
1425
  stage: Stage
1268
1426
 
1269
1427
 
1270
- # TODO: Not implement this stages yet
1271
1428
  class CaseStage(BaseStage): # pragma: no cov
1272
1429
  """Case execution stage.
1273
1430
 
@@ -1303,7 +1460,9 @@ class CaseStage(BaseStage): # pragma: no cov
1303
1460
  """
1304
1461
 
1305
1462
  case: str = Field(description="A case condition for routing.")
1306
- match: list[Match]
1463
+ match: list[Match] = Field(
1464
+ description="A list of Match model that should not be an empty list.",
1465
+ )
1307
1466
 
1308
1467
  def execute(
1309
1468
  self,
@@ -1326,10 +1485,14 @@ class CaseStage(BaseStage): # pragma: no cov
1326
1485
  result: Result = Result(
1327
1486
  run_id=gen_id(self.name + (self.id or ""), unique=True)
1328
1487
  )
1329
- status = SUCCESS
1488
+
1330
1489
  _case = param2template(self.case, params, extras=self.extras)
1490
+
1491
+ result.trace.info(f"[STAGE]: Case-Execute: {_case!r}.")
1331
1492
  _else = None
1493
+ stage: Optional[Stage] = None
1332
1494
  context = {}
1495
+ status = SUCCESS
1333
1496
  for match in self.match:
1334
1497
  if (c := match.case) != "_":
1335
1498
  _condition = param2template(c, params, extras=self.extras)
@@ -1337,27 +1500,35 @@ class CaseStage(BaseStage): # pragma: no cov
1337
1500
  _else = match
1338
1501
  continue
1339
1502
 
1340
- if match == _condition:
1503
+ if stage is None and _case == _condition:
1341
1504
  stage: Stage = match.stage
1342
- if self.extras:
1343
- stage.extras = self.extras
1344
-
1345
- try:
1346
- stage.set_outputs(
1347
- stage.handler_execute(
1348
- params=params,
1349
- run_id=result.run_id,
1350
- parent_run_id=result.parent_run_id,
1351
- ).context,
1352
- to=context,
1353
- )
1354
- except StageException as e: # pragma: no cov
1355
- status = FAILED
1356
- result.trace.error(
1357
- f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1358
- )
1359
- context.update({"errors": e.to_dict()})
1360
1505
 
1506
+ if stage is None:
1507
+ if _else is None:
1508
+ raise StageException(
1509
+ "This stage does not set else for support not match "
1510
+ "any case."
1511
+ )
1512
+
1513
+ stage: Stage = _else.stage
1514
+
1515
+ if self.extras:
1516
+ stage.extras = self.extras
1517
+
1518
+ try:
1519
+ context.update(
1520
+ stage.handler_execute(
1521
+ params=params,
1522
+ run_id=result.run_id,
1523
+ parent_run_id=result.parent_run_id,
1524
+ ).context
1525
+ )
1526
+ except StageException as e: # pragma: no cov
1527
+ status = FAILED
1528
+ result.trace.error(
1529
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1530
+ )
1531
+ context.update({"errors": e.to_dict()})
1361
1532
  return result.catch(status=status, context=context)
1362
1533
 
1363
1534
 
@@ -1408,7 +1579,8 @@ class HookStage(BaseStage): # pragma: no cov
1408
1579
  *,
1409
1580
  result: Result | None = None,
1410
1581
  event: Event | None = None,
1411
- ) -> Result: ...
1582
+ ) -> Result:
1583
+ raise NotImplementedError("Hook Stage does not implement yet.")
1412
1584
 
1413
1585
 
1414
1586
  # TODO: Not implement this stages yet
@@ -1441,15 +1613,20 @@ class DockerStage(BaseStage): # pragma: no cov
1441
1613
  *,
1442
1614
  result: Result | None = None,
1443
1615
  event: Event | None = None,
1444
- ) -> Result: ...
1616
+ ) -> Result:
1617
+ raise NotImplementedError("Docker Stage does not implement yet.")
1445
1618
 
1446
1619
 
1447
1620
  # TODO: Not implement this stages yet
1448
1621
  class VirtualPyStage(PyStage): # pragma: no cov
1449
1622
  """Python Virtual Environment stage execution."""
1450
1623
 
1451
- run: str
1452
- vars: DictData
1624
+ deps: list[str] = Field(
1625
+ description=(
1626
+ "list of Python dependency that want to install before execution "
1627
+ "stage."
1628
+ ),
1629
+ )
1453
1630
 
1454
1631
  def create_py_file(self, py: str, run_id: str | None): ...
1455
1632
 
@@ -1460,7 +1637,25 @@ class VirtualPyStage(PyStage): # pragma: no cov
1460
1637
  result: Result | None = None,
1461
1638
  event: Event | None = None,
1462
1639
  ) -> Result:
1463
- return super().execute(params, result=result)
1640
+ """Execute the Python statement via Python virtual environment.
1641
+
1642
+ :param params: A parameter that want to pass before run any statement.
1643
+ :param result: (Result) A result object for keeping context and status
1644
+ data.
1645
+ :param event: (Event) An event manager that use to track parent execute
1646
+ was not force stopped.
1647
+
1648
+ :rtype: Result
1649
+ """
1650
+ if result is None: # pragma: no cov
1651
+ result: Result = Result(
1652
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1653
+ )
1654
+
1655
+ result.trace.info(f"[STAGE]: Py-Virtual-Execute: {self.name}")
1656
+ raise NotImplementedError(
1657
+ "Python Virtual Stage does not implement yet."
1658
+ )
1464
1659
 
1465
1660
 
1466
1661
  # NOTE:
@@ -1470,11 +1665,16 @@ class VirtualPyStage(PyStage): # pragma: no cov
1470
1665
  #
1471
1666
  Stage = Annotated[
1472
1667
  Union[
1668
+ DockerStage,
1473
1669
  BashStage,
1474
1670
  CallStage,
1671
+ HookStage,
1475
1672
  TriggerStage,
1476
1673
  ForEachStage,
1674
+ UntilStage,
1477
1675
  ParallelStage,
1676
+ CaseStage,
1677
+ VirtualPyStage,
1478
1678
  PyStage,
1479
1679
  RaiseStage,
1480
1680
  EmptyStage,
ddeutil/workflow/utils.py CHANGED
@@ -15,7 +15,7 @@ from inspect import isfunction
15
15
  from itertools import chain, islice, product
16
16
  from pathlib import Path
17
17
  from random import randrange
18
- from typing import Any, TypeVar
18
+ from typing import Any, Final, TypeVar
19
19
  from zoneinfo import ZoneInfo
20
20
 
21
21
  from ddeutil.core import hash_str
@@ -23,7 +23,7 @@ from ddeutil.core import hash_str
23
23
  from .__types import DictData, Matrix
24
24
 
25
25
  T = TypeVar("T")
26
- UTC = ZoneInfo("UTC")
26
+ UTC: Final[ZoneInfo] = ZoneInfo("UTC")
27
27
 
28
28
 
29
29
  def get_dt_now(
@@ -142,7 +142,7 @@ def gen_id(
142
142
  if not isinstance(value, str):
143
143
  value: str = str(value)
144
144
 
145
- if config.gen_id_simple_mode:
145
+ if config.generate_id_simple_mode:
146
146
  return (
147
147
  f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}T" if unique else ""
148
148
  ) + hash_str(f"{(value if sensitive else value.lower())}", n=10)