ddeutil-workflow 0.0.49__py3-none-any.whl → 0.0.50__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/__about__.py +1 -1
- ddeutil/workflow/__init__.py +4 -0
- ddeutil/workflow/conf.py +9 -9
- ddeutil/workflow/exceptions.py +3 -3
- ddeutil/workflow/job.py +122 -71
- ddeutil/workflow/logs.py +1 -1
- ddeutil/workflow/result.py +12 -4
- ddeutil/workflow/stages.py +315 -115
- ddeutil/workflow/utils.py +42 -38
- ddeutil/workflow/workflow.py +18 -25
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.50.dist-info}/METADATA +3 -2
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.50.dist-info}/RECORD +15 -15
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.50.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.50.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.50.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
26
26
|
|
27
27
|
import asyncio
|
28
28
|
import contextlib
|
29
|
+
import copy
|
29
30
|
import inspect
|
30
31
|
import subprocess
|
31
32
|
import sys
|
@@ -34,10 +35,13 @@ import uuid
|
|
34
35
|
from abc import ABC, abstractmethod
|
35
36
|
from collections.abc import Iterator
|
36
37
|
from concurrent.futures import (
|
38
|
+
FIRST_EXCEPTION,
|
37
39
|
Future,
|
38
40
|
ThreadPoolExecutor,
|
39
41
|
as_completed,
|
42
|
+
wait,
|
40
43
|
)
|
44
|
+
from datetime import datetime
|
41
45
|
from inspect import Parameter
|
42
46
|
from pathlib import Path
|
43
47
|
from subprocess import CompletedProcess
|
@@ -51,10 +55,11 @@ from typing_extensions import Self
|
|
51
55
|
|
52
56
|
from .__types import DictData, DictStr, TupleStr
|
53
57
|
from .conf import dynamic
|
54
|
-
from .exceptions import StageException, to_dict
|
55
|
-
from .result import FAILED, SUCCESS, Result, Status
|
58
|
+
from .exceptions import StageException, UtilException, to_dict
|
59
|
+
from .result import FAILED, SUCCESS, WAIT, Result, Status
|
56
60
|
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
57
61
|
from .utils import (
|
62
|
+
filter_func,
|
58
63
|
gen_id,
|
59
64
|
make_exec,
|
60
65
|
)
|
@@ -150,9 +155,8 @@ class BaseStage(BaseModel, ABC):
|
|
150
155
|
parent_run_id: str | None = None,
|
151
156
|
result: Result | None = None,
|
152
157
|
raise_error: bool | None = None,
|
153
|
-
to: DictData | None = None,
|
154
158
|
event: Event | None = None,
|
155
|
-
) -> Result:
|
159
|
+
) -> Result | DictData:
|
156
160
|
"""Handler stage execution result from the stage `execute` method.
|
157
161
|
|
158
162
|
This stage exception handler still use ok-error concept, but it
|
@@ -184,8 +188,6 @@ class BaseStage(BaseModel, ABC):
|
|
184
188
|
:param result: (Result) A result object for keeping context and status
|
185
189
|
data before execution.
|
186
190
|
:param raise_error: (bool) A flag that all this method raise error
|
187
|
-
:param to: (DictData) A target object for auto set the return output
|
188
|
-
after execution.
|
189
191
|
:param event: (Event) An event manager that pass to the stage execution.
|
190
192
|
|
191
193
|
:rtype: Result
|
@@ -200,7 +202,7 @@ class BaseStage(BaseModel, ABC):
|
|
200
202
|
|
201
203
|
try:
|
202
204
|
rs: Result = self.execute(params, result=result, event=event)
|
203
|
-
return
|
205
|
+
return rs
|
204
206
|
except Exception as e:
|
205
207
|
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
206
208
|
|
@@ -214,11 +216,7 @@ class BaseStage(BaseModel, ABC):
|
|
214
216
|
) from e
|
215
217
|
|
216
218
|
errors: DictData = {"errors": to_dict(e)}
|
217
|
-
return (
|
218
|
-
self.set_outputs(errors, to=to)
|
219
|
-
if to is not None
|
220
|
-
else result.catch(status=FAILED, context=errors)
|
221
|
-
)
|
219
|
+
return result.catch(status=FAILED, context=errors)
|
222
220
|
|
223
221
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
224
222
|
"""Set an outputs from execution process to the received context. The
|
@@ -275,7 +273,10 @@ class BaseStage(BaseModel, ABC):
|
|
275
273
|
return to
|
276
274
|
|
277
275
|
def get_outputs(self, outputs: DictData) -> DictData:
|
278
|
-
"""Get the outputs from stages data.
|
276
|
+
"""Get the outputs from stages data. It will get this stage ID from
|
277
|
+
the stage outputs mapping.
|
278
|
+
|
279
|
+
:param outputs: (DictData) A stage outputs that want to get by stage ID.
|
279
280
|
|
280
281
|
:rtype: DictData
|
281
282
|
"""
|
@@ -374,7 +375,6 @@ class BaseAsyncStage(BaseStage):
|
|
374
375
|
parent_run_id: str | None = None,
|
375
376
|
result: Result | None = None,
|
376
377
|
raise_error: bool | None = None,
|
377
|
-
to: DictData | None = None,
|
378
378
|
event: Event | None = None,
|
379
379
|
) -> Result:
|
380
380
|
"""Async Handler stage execution result from the stage `execute` method.
|
@@ -387,8 +387,6 @@ class BaseAsyncStage(BaseStage):
|
|
387
387
|
:param result: (Result) A result object for keeping context and status
|
388
388
|
data before execution.
|
389
389
|
:param raise_error: (bool) A flag that all this method raise error
|
390
|
-
:param to: (DictData) A target object for auto set the return output
|
391
|
-
after execution.
|
392
390
|
:param event: (Event) An event manager that pass to the stage execution.
|
393
391
|
|
394
392
|
:rtype: Result
|
@@ -403,8 +401,6 @@ class BaseAsyncStage(BaseStage):
|
|
403
401
|
|
404
402
|
try:
|
405
403
|
rs: Result = await self.axecute(params, result=result, event=event)
|
406
|
-
if to is not None: # pragma: no cov
|
407
|
-
return self.set_outputs(rs.context, to=to)
|
408
404
|
return rs
|
409
405
|
except Exception as e: # pragma: no cov
|
410
406
|
await result.trace.aerror(f"[STAGE]: {e.__class__.__name__}: {e}")
|
@@ -419,9 +415,6 @@ class BaseAsyncStage(BaseStage):
|
|
419
415
|
) from None
|
420
416
|
|
421
417
|
errors: DictData = {"errors": to_dict(e)}
|
422
|
-
if to is not None:
|
423
|
-
return self.set_outputs(errors, to=to)
|
424
|
-
|
425
418
|
return result.catch(status=FAILED, context=errors)
|
426
419
|
|
427
420
|
|
@@ -880,7 +873,7 @@ class CallStage(BaseStage):
|
|
880
873
|
# VALIDATE:
|
881
874
|
# Check the result type from call function, it should be dict.
|
882
875
|
if isinstance(rs, BaseModel):
|
883
|
-
rs: DictData = rs.model_dump()
|
876
|
+
rs: DictData = rs.model_dump(by_alias=True)
|
884
877
|
elif not isinstance(rs, dict):
|
885
878
|
raise TypeError(
|
886
879
|
f"Return type: '{call_func.name}@{call_func.tag}' does not "
|
@@ -896,6 +889,10 @@ class CallStage(BaseStage):
|
|
896
889
|
) -> DictData:
|
897
890
|
"""Parse Pydantic model from any dict data before parsing to target
|
898
891
|
caller function.
|
892
|
+
|
893
|
+
:param func:
|
894
|
+
:param args:
|
895
|
+
:param result: (Result)
|
899
896
|
"""
|
900
897
|
try:
|
901
898
|
type_hints: dict[str, Any] = get_type_hints(func)
|
@@ -942,7 +939,7 @@ class TriggerStage(BaseStage):
|
|
942
939
|
|
943
940
|
trigger: str = Field(
|
944
941
|
description=(
|
945
|
-
"A trigger workflow name that should
|
942
|
+
"A trigger workflow name that should exist on the config path."
|
946
943
|
),
|
947
944
|
)
|
948
945
|
params: DictData = Field(
|
@@ -968,26 +965,30 @@ class TriggerStage(BaseStage):
|
|
968
965
|
|
969
966
|
:rtype: Result
|
970
967
|
"""
|
971
|
-
|
972
|
-
from . import Workflow
|
968
|
+
from .workflow import Workflow
|
973
969
|
|
974
|
-
if result is None:
|
970
|
+
if result is None:
|
975
971
|
result: Result = Result(
|
976
972
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
977
973
|
)
|
978
974
|
|
979
|
-
# NOTE: Loading workflow object from trigger name.
|
980
975
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
981
|
-
|
982
|
-
# NOTE: Set running workflow ID from running stage ID to external
|
983
|
-
# params on Loader object.
|
984
|
-
workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
|
985
976
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
986
|
-
|
977
|
+
rs: Result = Workflow.from_conf(
|
978
|
+
_trigger, extras=self.extras | {"stage_raise_error": True}
|
979
|
+
).execute(
|
987
980
|
params=param2template(self.params, params, extras=self.extras),
|
988
981
|
result=result,
|
989
982
|
event=event,
|
990
983
|
)
|
984
|
+
if rs.status == FAILED:
|
985
|
+
err_msg: str | None = (
|
986
|
+
f" with:\n{msg}"
|
987
|
+
if (msg := rs.context.get("errors", {}).get("message"))
|
988
|
+
else ""
|
989
|
+
)
|
990
|
+
raise StageException(f"Trigger workflow was failed{err_msg}.")
|
991
|
+
return rs
|
991
992
|
|
992
993
|
|
993
994
|
class ParallelStage(BaseStage): # pragma: no cov
|
@@ -1124,10 +1125,10 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1124
1125
|
|
1125
1126
|
class ForEachStage(BaseStage):
|
1126
1127
|
"""For-Each execution stage that execute child stages with an item in list
|
1127
|
-
of item values.
|
1128
|
+
of item values. This stage is not the low-level stage model because it runs
|
1129
|
+
muti-stages in this stage execution.
|
1128
1130
|
|
1129
|
-
|
1130
|
-
in this stage execution.
|
1131
|
+
The concept of this stage use the same logic of the Job execution.
|
1131
1132
|
|
1132
1133
|
Data Validate:
|
1133
1134
|
>>> stage = {
|
@@ -1167,46 +1168,91 @@ class ForEachStage(BaseStage):
|
|
1167
1168
|
self,
|
1168
1169
|
item: Union[str, int],
|
1169
1170
|
params: DictData,
|
1170
|
-
context: DictData,
|
1171
1171
|
result: Result,
|
1172
|
-
|
1172
|
+
*,
|
1173
|
+
event: Event | None = None,
|
1174
|
+
) -> Result:
|
1173
1175
|
"""Execute foreach item from list of item.
|
1174
1176
|
|
1175
1177
|
:param item: (str | int) An item that want to execution.
|
1176
1178
|
:param params: (DictData) A parameter that want to pass to stage
|
1177
1179
|
execution.
|
1178
|
-
:param
|
1179
|
-
|
1180
|
+
:param result: (Result) A result object for keeping context and status
|
1181
|
+
data.
|
1182
|
+
:param event: (Event) An event manager that use to track parent execute
|
1183
|
+
was not force stopped.
|
1180
1184
|
|
1181
|
-
:rtype:
|
1185
|
+
:rtype: Result
|
1182
1186
|
"""
|
1183
|
-
result.trace.debug(f"[STAGE]: Execute
|
1184
|
-
|
1185
|
-
|
1186
|
-
status: Status = SUCCESS
|
1187
|
+
result.trace.debug(f"[STAGE]: Execute item: {item!r}")
|
1188
|
+
context: DictData = copy.deepcopy(params)
|
1189
|
+
context.update({"item": item, "stages": {}})
|
1187
1190
|
for stage in self.stages:
|
1188
1191
|
|
1189
1192
|
if self.extras:
|
1190
1193
|
stage.extras = self.extras
|
1191
1194
|
|
1192
|
-
|
1193
|
-
stage.
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1195
|
+
if stage.is_skipped(params=context):
|
1196
|
+
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
1197
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
1198
|
+
continue
|
1199
|
+
|
1200
|
+
if event and event.is_set():
|
1201
|
+
error_msg: str = (
|
1202
|
+
"Item-Stage was canceled from event that had set before "
|
1203
|
+
"stage item execution."
|
1200
1204
|
)
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
+
return result.catch(
|
1206
|
+
status=FAILED,
|
1207
|
+
foreach={
|
1208
|
+
item: {
|
1209
|
+
"item": item,
|
1210
|
+
"stages": filter_func(context.pop("stages", {})),
|
1211
|
+
"errors": StageException(error_msg).to_dict(),
|
1212
|
+
}
|
1213
|
+
},
|
1214
|
+
)
|
1215
|
+
|
1216
|
+
try:
|
1217
|
+
rs: Result = stage.handler_execute(
|
1218
|
+
params=context,
|
1219
|
+
run_id=result.run_id,
|
1220
|
+
parent_run_id=result.parent_run_id,
|
1221
|
+
raise_error=True,
|
1205
1222
|
)
|
1206
|
-
|
1223
|
+
stage.set_outputs(rs.context, to=context)
|
1224
|
+
if rs.status == FAILED:
|
1225
|
+
error_msg: str = (
|
1226
|
+
f"Item-Stage was break because it has a sub stage, "
|
1227
|
+
f"{stage.iden}, failed without raise error."
|
1228
|
+
)
|
1229
|
+
return result.catch(
|
1230
|
+
status=FAILED,
|
1231
|
+
foreach={
|
1232
|
+
item: {
|
1233
|
+
"item": item,
|
1234
|
+
"stages": filter_func(
|
1235
|
+
context.pop("stages", {})
|
1236
|
+
),
|
1237
|
+
"errors": StageException(error_msg).to_dict(),
|
1238
|
+
},
|
1239
|
+
},
|
1240
|
+
)
|
1241
|
+
except (StageException, UtilException) as e:
|
1242
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
1243
|
+
raise StageException(
|
1244
|
+
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1245
|
+
) from None
|
1207
1246
|
|
1208
|
-
|
1209
|
-
|
1247
|
+
return result.catch(
|
1248
|
+
status=SUCCESS,
|
1249
|
+
foreach={
|
1250
|
+
item: {
|
1251
|
+
"item": item,
|
1252
|
+
"stages": filter_func(context.pop("stages", {})),
|
1253
|
+
},
|
1254
|
+
},
|
1255
|
+
)
|
1210
1256
|
|
1211
1257
|
def execute(
|
1212
1258
|
self,
|
@@ -1227,9 +1273,10 @@ class ForEachStage(BaseStage):
|
|
1227
1273
|
"""
|
1228
1274
|
if result is None: # pragma: no cov
|
1229
1275
|
result: Result = Result(
|
1230
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1276
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1277
|
+
extras=self.extras,
|
1231
1278
|
)
|
1232
|
-
|
1279
|
+
event: Event = Event() if event is None else event
|
1233
1280
|
foreach: Union[list[str], list[int]] = (
|
1234
1281
|
param2template(self.foreach, params, extras=self.extras)
|
1235
1282
|
if isinstance(self.foreach, str)
|
@@ -1241,24 +1288,57 @@ class ForEachStage(BaseStage):
|
|
1241
1288
|
)
|
1242
1289
|
|
1243
1290
|
result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
|
1244
|
-
context
|
1245
|
-
|
1246
|
-
|
1291
|
+
result.catch(status=WAIT, context={"items": foreach, "foreach": {}})
|
1292
|
+
if event and event.is_set(): # pragma: no cov
|
1293
|
+
return result.catch(
|
1294
|
+
status=FAILED,
|
1295
|
+
context={
|
1296
|
+
"errors": StageException(
|
1297
|
+
"Stage was canceled from event that had set "
|
1298
|
+
"before stage foreach execution."
|
1299
|
+
).to_dict()
|
1300
|
+
},
|
1301
|
+
)
|
1302
|
+
|
1303
|
+
with ThreadPoolExecutor(
|
1304
|
+
max_workers=self.concurrent, thread_name_prefix="stage_foreach_"
|
1305
|
+
) as executor:
|
1306
|
+
|
1247
1307
|
futures: list[Future] = [
|
1248
1308
|
executor.submit(
|
1249
1309
|
self.execute_item,
|
1250
1310
|
item=item,
|
1251
|
-
params=params
|
1252
|
-
context=context,
|
1311
|
+
params=params,
|
1253
1312
|
result=result,
|
1254
1313
|
)
|
1255
1314
|
for item in foreach
|
1256
1315
|
]
|
1257
|
-
|
1258
|
-
|
1259
|
-
statuses.append(status)
|
1316
|
+
context: DictData = {}
|
1317
|
+
status: Status = SUCCESS
|
1260
1318
|
|
1261
|
-
|
1319
|
+
done, not_done = wait(
|
1320
|
+
futures, timeout=1800, return_when=FIRST_EXCEPTION
|
1321
|
+
)
|
1322
|
+
|
1323
|
+
if len(done) != len(futures):
|
1324
|
+
result.trace.warning(
|
1325
|
+
"[STAGE]: Set the event for stop running stage."
|
1326
|
+
)
|
1327
|
+
event.set()
|
1328
|
+
for future in not_done:
|
1329
|
+
future.cancel()
|
1330
|
+
|
1331
|
+
for future in done:
|
1332
|
+
try:
|
1333
|
+
future.result()
|
1334
|
+
except StageException as e:
|
1335
|
+
status = FAILED
|
1336
|
+
result.trace.error(
|
1337
|
+
f"[STAGE]: Catch:\n\t{e.__class__.__name__}:\n\t{e}"
|
1338
|
+
)
|
1339
|
+
context.update({"errors": e.to_dict()})
|
1340
|
+
|
1341
|
+
return result.catch(status=status, context=context)
|
1262
1342
|
|
1263
1343
|
|
1264
1344
|
class UntilStage(BaseStage): # pragma: no cov
|
@@ -1299,9 +1379,9 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1299
1379
|
item: T,
|
1300
1380
|
loop: int,
|
1301
1381
|
params: DictData,
|
1302
|
-
context: DictData,
|
1303
1382
|
result: Result,
|
1304
|
-
|
1383
|
+
event: Event | None = None,
|
1384
|
+
) -> tuple[Result, T]:
|
1305
1385
|
"""Execute until item set item by some stage or by default loop
|
1306
1386
|
variable.
|
1307
1387
|
|
@@ -1309,43 +1389,80 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1309
1389
|
:param loop: (int) A number of loop.
|
1310
1390
|
:param params: (DictData) A parameter that want to pass to stage
|
1311
1391
|
execution.
|
1312
|
-
:param
|
1313
|
-
|
1392
|
+
:param result: (Result) A result object for keeping context and status
|
1393
|
+
data.
|
1394
|
+
:param event: (Event) An event manager that use to track parent execute
|
1395
|
+
was not force stopped.
|
1314
1396
|
|
1315
|
-
:rtype: tuple[
|
1397
|
+
:rtype: tuple[Result, T]
|
1316
1398
|
"""
|
1317
1399
|
result.trace.debug(f"[STAGE]: Execute until item: {item!r}")
|
1318
|
-
|
1319
|
-
|
1320
|
-
status: Status = SUCCESS
|
1400
|
+
context: DictData = copy.deepcopy(params)
|
1401
|
+
context.update({"loop": loop, "item": item, "stages": {}})
|
1321
1402
|
next_item: T = None
|
1322
1403
|
for stage in self.stages:
|
1323
1404
|
|
1324
1405
|
if self.extras:
|
1325
1406
|
stage.extras = self.extras
|
1326
1407
|
|
1408
|
+
if stage.is_skipped(params=context):
|
1409
|
+
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
1410
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
1411
|
+
continue
|
1412
|
+
|
1413
|
+
if event and event.is_set():
|
1414
|
+
error_msg: str = (
|
1415
|
+
"Item-Stage was canceled from event that had set before "
|
1416
|
+
"stage item execution."
|
1417
|
+
)
|
1418
|
+
return (
|
1419
|
+
result.catch(
|
1420
|
+
status=FAILED,
|
1421
|
+
until={
|
1422
|
+
loop: {
|
1423
|
+
"loop": loop,
|
1424
|
+
"item": item,
|
1425
|
+
"stages": filter_func(
|
1426
|
+
context.pop("stages", {})
|
1427
|
+
),
|
1428
|
+
"errors": StageException(error_msg).to_dict(),
|
1429
|
+
}
|
1430
|
+
},
|
1431
|
+
),
|
1432
|
+
next_item,
|
1433
|
+
)
|
1434
|
+
|
1327
1435
|
try:
|
1328
|
-
stage.
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
).context,
|
1334
|
-
to=to,
|
1436
|
+
rs: Result = stage.handler_execute(
|
1437
|
+
params=context,
|
1438
|
+
run_id=result.run_id,
|
1439
|
+
parent_run_id=result.parent_run_id,
|
1440
|
+
raise_error=True,
|
1335
1441
|
)
|
1442
|
+
stage.set_outputs(rs.context, to=context)
|
1336
1443
|
if "item" in (
|
1337
|
-
outputs := stage.get_outputs(
|
1444
|
+
outputs := stage.get_outputs(context).get("outputs", {})
|
1338
1445
|
):
|
1339
1446
|
next_item = outputs["item"]
|
1340
|
-
except StageException as e:
|
1341
|
-
|
1342
|
-
|
1343
|
-
f"
|
1344
|
-
)
|
1345
|
-
to.update({"errors": e.to_dict()})
|
1447
|
+
except (StageException, UtilException) as e:
|
1448
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
1449
|
+
raise StageException(
|
1450
|
+
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1451
|
+
) from None
|
1346
1452
|
|
1347
|
-
|
1348
|
-
|
1453
|
+
return (
|
1454
|
+
result.catch(
|
1455
|
+
status=SUCCESS,
|
1456
|
+
until={
|
1457
|
+
loop: {
|
1458
|
+
"loop": loop,
|
1459
|
+
"item": item,
|
1460
|
+
"stages": filter_func(context.pop("stages", {})),
|
1461
|
+
}
|
1462
|
+
},
|
1463
|
+
),
|
1464
|
+
next_item,
|
1465
|
+
)
|
1349
1466
|
|
1350
1467
|
def execute(
|
1351
1468
|
self,
|
@@ -1369,22 +1486,31 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1369
1486
|
result: Result = Result(
|
1370
1487
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1371
1488
|
)
|
1372
|
-
|
1489
|
+
result.trace.info(f"[STAGE]: Until-Execution: {self.until}")
|
1373
1490
|
item: Union[str, int, bool] = param2template(
|
1374
1491
|
self.item, params, extras=self.extras
|
1375
1492
|
)
|
1376
|
-
|
1493
|
+
loop: int = 1
|
1377
1494
|
track: bool = True
|
1378
1495
|
exceed_loop: bool = False
|
1379
|
-
|
1380
|
-
context: DictData = {"until": {}}
|
1381
|
-
statuses: list[Union[SUCCESS, FAILED]] = []
|
1496
|
+
result.catch(status=WAIT, context={"until": {}})
|
1382
1497
|
while track and not (exceed_loop := loop >= self.max_until_loop):
|
1383
|
-
|
1498
|
+
|
1499
|
+
if event and event.is_set():
|
1500
|
+
return result.catch(
|
1501
|
+
status=FAILED,
|
1502
|
+
context={
|
1503
|
+
"errors": StageException(
|
1504
|
+
"Stage was canceled from event that had set "
|
1505
|
+
"before stage until execution."
|
1506
|
+
).to_dict()
|
1507
|
+
},
|
1508
|
+
)
|
1509
|
+
|
1510
|
+
result, item = self.execute_item(
|
1384
1511
|
item=item,
|
1385
1512
|
loop=loop,
|
1386
|
-
params=params
|
1387
|
-
context=context,
|
1513
|
+
params=params,
|
1388
1514
|
result=result,
|
1389
1515
|
)
|
1390
1516
|
|
@@ -1398,31 +1524,32 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1398
1524
|
|
1399
1525
|
next_track: bool = eval(
|
1400
1526
|
param2template(
|
1401
|
-
self.until,
|
1527
|
+
self.until,
|
1528
|
+
params | {"item": item, "loop": loop},
|
1529
|
+
extras=self.extras,
|
1402
1530
|
),
|
1403
1531
|
globals() | params | {"item": item},
|
1404
1532
|
{},
|
1405
1533
|
)
|
1406
1534
|
if not isinstance(next_track, bool):
|
1407
|
-
raise
|
1535
|
+
raise StageException(
|
1408
1536
|
"Return type of until condition does not be boolean, it"
|
1409
1537
|
f"return: {next_track!r}"
|
1410
1538
|
)
|
1411
1539
|
track = not next_track
|
1412
|
-
statuses.append(status)
|
1413
1540
|
|
1414
1541
|
if exceed_loop:
|
1415
1542
|
raise StageException(
|
1416
1543
|
f"The until loop was exceed {self.max_until_loop} loops"
|
1417
1544
|
)
|
1418
|
-
return result.catch(status=
|
1545
|
+
return result.catch(status=SUCCESS)
|
1419
1546
|
|
1420
1547
|
|
1421
1548
|
class Match(BaseModel):
|
1422
1549
|
"""Match model for the Case Stage."""
|
1423
1550
|
|
1424
|
-
case: Union[str, int]
|
1425
|
-
stage: Stage
|
1551
|
+
case: Union[str, int] = Field(description="A match case.")
|
1552
|
+
stage: Stage = Field(description="A stage to execution for this case.")
|
1426
1553
|
|
1427
1554
|
|
1428
1555
|
class CaseStage(BaseStage): # pragma: no cov
|
@@ -1533,8 +1660,8 @@ class CaseStage(BaseStage): # pragma: no cov
|
|
1533
1660
|
|
1534
1661
|
|
1535
1662
|
class RaiseStage(BaseStage): # pragma: no cov
|
1536
|
-
"""Raise error stage that raise StageException that use a message
|
1537
|
-
making error message before raise.
|
1663
|
+
"""Raise error stage execution that raise StageException that use a message
|
1664
|
+
field for making error message before raise.
|
1538
1665
|
|
1539
1666
|
Data Validate:
|
1540
1667
|
>>> stage = {
|
@@ -1556,13 +1683,21 @@ class RaiseStage(BaseStage): # pragma: no cov
|
|
1556
1683
|
result: Result | None = None,
|
1557
1684
|
event: Event | None = None,
|
1558
1685
|
) -> Result:
|
1559
|
-
"""Raise the
|
1686
|
+
"""Raise the StageException object with the message field execution.
|
1687
|
+
|
1688
|
+
:param params: A parameter that want to pass before run any statement.
|
1689
|
+
:param result: (Result) A result object for keeping context and status
|
1690
|
+
data.
|
1691
|
+
:param event: (Event) An event manager that use to track parent execute
|
1692
|
+
was not force stopped.
|
1693
|
+
"""
|
1560
1694
|
if result is None: # pragma: no cov
|
1561
1695
|
result: Result = Result(
|
1562
1696
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1563
1697
|
)
|
1564
|
-
|
1565
|
-
|
1698
|
+
message: str = param2template(self.message, params, extras=self.extras)
|
1699
|
+
result.trace.info(f"[STAGE]: Raise-Execute: {message!r}.")
|
1700
|
+
raise StageException(message)
|
1566
1701
|
|
1567
1702
|
|
1568
1703
|
# TODO: Not implement this stages yet
|
@@ -1570,7 +1705,7 @@ class HookStage(BaseStage): # pragma: no cov
|
|
1570
1705
|
"""Hook stage execution."""
|
1571
1706
|
|
1572
1707
|
hook: str
|
1573
|
-
args: DictData
|
1708
|
+
args: DictData = Field(default_factory=dict)
|
1574
1709
|
callback: str
|
1575
1710
|
|
1576
1711
|
def execute(
|
@@ -1603,9 +1738,69 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
1603
1738
|
image: str = Field(
|
1604
1739
|
description="A Docker image url with tag that want to run.",
|
1605
1740
|
)
|
1741
|
+
tag: str = Field(default="latest", description="An Docker image tag.")
|
1606
1742
|
env: DictData = Field(default_factory=dict)
|
1607
1743
|
volume: DictData = Field(default_factory=dict)
|
1608
|
-
auth: DictData = Field(
|
1744
|
+
auth: DictData = Field(
|
1745
|
+
default_factory=dict,
|
1746
|
+
description=(
|
1747
|
+
"An authentication of the Docker registry that use in pulling step."
|
1748
|
+
),
|
1749
|
+
)
|
1750
|
+
|
1751
|
+
def execute_task(
|
1752
|
+
self,
|
1753
|
+
result: Result,
|
1754
|
+
):
|
1755
|
+
from docker import DockerClient
|
1756
|
+
from docker.errors import ContainerError
|
1757
|
+
|
1758
|
+
client = DockerClient(
|
1759
|
+
base_url="unix://var/run/docker.sock", version="auto"
|
1760
|
+
)
|
1761
|
+
|
1762
|
+
resp = client.api.pull(
|
1763
|
+
repository=f"{self.image}",
|
1764
|
+
tag=self.tag,
|
1765
|
+
# NOTE: For pulling from GCS.
|
1766
|
+
# {
|
1767
|
+
# "username": "_json_key",
|
1768
|
+
# "password": credential-json-string,
|
1769
|
+
# }
|
1770
|
+
auth_config=self.auth,
|
1771
|
+
stream=True,
|
1772
|
+
decode=True,
|
1773
|
+
)
|
1774
|
+
for line in resp:
|
1775
|
+
result.trace.info(f"... {line}")
|
1776
|
+
|
1777
|
+
unique_image_name: str = f"{self.image}_{datetime.now():%Y%m%d%H%M%S}"
|
1778
|
+
container = client.containers.run(
|
1779
|
+
image=f"{self.image}:{self.tag}",
|
1780
|
+
name=unique_image_name,
|
1781
|
+
environment=self.env,
|
1782
|
+
volumes=(
|
1783
|
+
{Path.cwd() / ".docker.logs": {"bind": "/logs", "mode": "rw"}}
|
1784
|
+
| self.volume
|
1785
|
+
),
|
1786
|
+
detach=True,
|
1787
|
+
)
|
1788
|
+
|
1789
|
+
for line in container.logs(stream=True, timestamps=True):
|
1790
|
+
result.trace.info(f"... {line.strip().decode()}")
|
1791
|
+
|
1792
|
+
# NOTE: This code copy from the docker package.
|
1793
|
+
exit_status: int = container.wait()["StatusCode"]
|
1794
|
+
if exit_status != 0:
|
1795
|
+
out = container.logs(stdout=False, stderr=True)
|
1796
|
+
container.remove()
|
1797
|
+
raise ContainerError(
|
1798
|
+
container,
|
1799
|
+
exit_status,
|
1800
|
+
None,
|
1801
|
+
f"{self.image}:{self.tag}",
|
1802
|
+
out,
|
1803
|
+
)
|
1609
1804
|
|
1610
1805
|
def execute(
|
1611
1806
|
self,
|
@@ -1639,6 +1834,11 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
1639
1834
|
) -> Result:
|
1640
1835
|
"""Execute the Python statement via Python virtual environment.
|
1641
1836
|
|
1837
|
+
Steps:
|
1838
|
+
- Create python file.
|
1839
|
+
- Create `.venv` and install necessary Python deps.
|
1840
|
+
- Execution python file with uv and specific `.venv`.
|
1841
|
+
|
1642
1842
|
:param params: A parameter that want to pass before run any statement.
|
1643
1843
|
:param result: (Result) A result object for keeping context and status
|
1644
1844
|
data.
|