ddeutil-workflow 0.0.36__py3-none-any.whl → 0.0.38__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 -1
- ddeutil/workflow/api/api.py +3 -1
- ddeutil/workflow/api/log.py +59 -0
- ddeutil/workflow/api/repeat.py +1 -1
- ddeutil/workflow/api/routes/job.py +4 -2
- ddeutil/workflow/api/routes/logs.py +126 -17
- ddeutil/workflow/api/routes/schedules.py +6 -6
- ddeutil/workflow/api/routes/workflows.py +9 -7
- ddeutil/workflow/caller.py +9 -3
- ddeutil/workflow/conf.py +0 -60
- ddeutil/workflow/context.py +59 -0
- ddeutil/workflow/exceptions.py +14 -1
- ddeutil/workflow/job.py +310 -277
- ddeutil/workflow/logs.py +6 -1
- ddeutil/workflow/result.py +1 -1
- ddeutil/workflow/scheduler.py +11 -4
- ddeutil/workflow/stages.py +368 -111
- ddeutil/workflow/utils.py +27 -49
- ddeutil/workflow/workflow.py +137 -72
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/METADATA +12 -6
- ddeutil_workflow-0.0.38.dist-info/RECORD +33 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.36.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info/licenses}/LICENSE +0 -0
- {ddeutil_workflow-0.0.36.dist-info → ddeutil_workflow-0.0.38.dist-info}/top_level.txt +0 -0
ddeutil/workflow/utils.py
CHANGED
@@ -5,10 +5,9 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import logging
|
9
8
|
import stat
|
10
9
|
import time
|
11
|
-
from collections.abc import Iterator
|
10
|
+
from collections.abc import Iterator
|
12
11
|
from datetime import date, datetime, timedelta
|
13
12
|
from hashlib import md5
|
14
13
|
from inspect import isfunction
|
@@ -24,7 +23,6 @@ from .__types import DictData, Matrix
|
|
24
23
|
|
25
24
|
T = TypeVar("T")
|
26
25
|
UTC = ZoneInfo("UTC")
|
27
|
-
logger = logging.getLogger("ddeutil.workflow")
|
28
26
|
|
29
27
|
|
30
28
|
def get_dt_now(
|
@@ -32,8 +30,10 @@ def get_dt_now(
|
|
32
30
|
) -> datetime: # pragma: no cov
|
33
31
|
"""Return the current datetime object.
|
34
32
|
|
35
|
-
:param tz:
|
36
|
-
:param offset:
|
33
|
+
:param tz: A ZoneInfo object for replace timezone of return datetime object.
|
34
|
+
:param offset: An offset second value.
|
35
|
+
|
36
|
+
:rtype: datetime
|
37
37
|
:return: The current datetime object that use an input timezone or UTC.
|
38
38
|
"""
|
39
39
|
return datetime.now(tz=(tz or UTC)) - timedelta(seconds=offset)
|
@@ -42,6 +42,14 @@ def get_dt_now(
|
|
42
42
|
def get_d_now(
|
43
43
|
tz: ZoneInfo | None = None, offset: float = 0.0
|
44
44
|
) -> date: # pragma: no cov
|
45
|
+
"""Return the current date object.
|
46
|
+
|
47
|
+
:param tz: A ZoneInfo object for replace timezone of return date object.
|
48
|
+
:param offset: An offset second value.
|
49
|
+
|
50
|
+
:rtype: date
|
51
|
+
:return: The current date object that use an input timezone or UTC.
|
52
|
+
"""
|
45
53
|
return (datetime.now(tz=(tz or UTC)) - timedelta(seconds=offset)).date()
|
46
54
|
|
47
55
|
|
@@ -52,8 +60,10 @@ def get_diff_sec(
|
|
52
60
|
current datetime with specific timezone.
|
53
61
|
|
54
62
|
:param dt:
|
55
|
-
:param tz:
|
56
|
-
:param offset:
|
63
|
+
:param tz: A ZoneInfo object for replace timezone of return datetime object.
|
64
|
+
:param offset: An offset second value.
|
65
|
+
|
66
|
+
:rtype: int
|
57
67
|
"""
|
58
68
|
return round(
|
59
69
|
(
|
@@ -67,6 +77,10 @@ def reach_next_minute(
|
|
67
77
|
) -> bool:
|
68
78
|
"""Check this datetime object is not in range of minute level on the current
|
69
79
|
datetime.
|
80
|
+
|
81
|
+
:param dt:
|
82
|
+
:param tz: A ZoneInfo object for replace timezone of return datetime object.
|
83
|
+
:param offset: An offset second value.
|
70
84
|
"""
|
71
85
|
diff: float = (
|
72
86
|
dt.replace(second=0, microsecond=0)
|
@@ -128,13 +142,14 @@ def gen_id(
|
|
128
142
|
value: str = str(value)
|
129
143
|
|
130
144
|
if config.gen_id_simple_mode:
|
131
|
-
return
|
132
|
-
f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}" if unique else ""
|
133
|
-
)
|
145
|
+
return (
|
146
|
+
f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}T" if unique else ""
|
147
|
+
) + hash_str(f"{(value if sensitive else value.lower())}", n=10)
|
148
|
+
|
134
149
|
return md5(
|
135
150
|
(
|
136
|
-
f"{(
|
137
|
-
+
|
151
|
+
(f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}T" if unique else "")
|
152
|
+
+ f"{(value if sensitive else value.lower())}"
|
138
153
|
).encode()
|
139
154
|
).hexdigest()
|
140
155
|
|
@@ -171,25 +186,6 @@ def filter_func(value: T) -> T:
|
|
171
186
|
return value
|
172
187
|
|
173
188
|
|
174
|
-
def dash2underscore(
|
175
|
-
key: str,
|
176
|
-
values: DictData,
|
177
|
-
*,
|
178
|
-
fixed: str | None = None,
|
179
|
-
) -> DictData:
|
180
|
-
"""Change key name that has dash to underscore.
|
181
|
-
|
182
|
-
:param key
|
183
|
-
:param values
|
184
|
-
:param fixed
|
185
|
-
|
186
|
-
:rtype: DictData
|
187
|
-
"""
|
188
|
-
if key in values:
|
189
|
-
values[(fixed or key.replace("-", "_"))] = values.pop(key)
|
190
|
-
return values
|
191
|
-
|
192
|
-
|
193
189
|
def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
194
190
|
"""Iterator of products value from matrix.
|
195
191
|
|
@@ -246,21 +242,3 @@ def cut_id(run_id: str, *, num: int = 6) -> str:
|
|
246
242
|
:rtype: str
|
247
243
|
"""
|
248
244
|
return run_id[-num:]
|
249
|
-
|
250
|
-
|
251
|
-
def deep_update(origin: DictData, u: Mapping) -> DictData:
|
252
|
-
"""Deep update dict.
|
253
|
-
|
254
|
-
Example:
|
255
|
-
>>> deep_update(
|
256
|
-
... origin={"jobs": {"job01": "foo"}},
|
257
|
-
... u={"jobs": {"job02": "bar"}},
|
258
|
-
... )
|
259
|
-
{"jobs": {"job01": "foo", "job02": "bar"}}
|
260
|
-
"""
|
261
|
-
for k, value in u.items():
|
262
|
-
if isinstance(value, Mapping) and value:
|
263
|
-
origin[k] = deep_update(origin.get(k, {}), value)
|
264
|
-
else:
|
265
|
-
origin[k] = value
|
266
|
-
return origin
|
ddeutil/workflow/workflow.py
CHANGED
@@ -20,6 +20,7 @@ from functools import partial, total_ordering
|
|
20
20
|
from heapq import heappop, heappush
|
21
21
|
from queue import Queue
|
22
22
|
from textwrap import dedent
|
23
|
+
from threading import Event
|
23
24
|
from typing import Optional
|
24
25
|
|
25
26
|
from pydantic import BaseModel, ConfigDict, Field
|
@@ -68,34 +69,45 @@ class ReleaseType(str, Enum):
|
|
68
69
|
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
|
69
70
|
)
|
70
71
|
class Release:
|
71
|
-
"""Release Pydantic dataclass object that use for represent
|
72
|
-
|
72
|
+
"""Release Pydantic dataclass object that use for represent the release data
|
73
|
+
that use with the `workflow.release` method.
|
74
|
+
"""
|
73
75
|
|
74
|
-
date: datetime
|
75
|
-
offset: float
|
76
|
-
end_date: datetime
|
77
|
-
runner: CronRunner
|
76
|
+
date: datetime = field()
|
77
|
+
offset: float = field()
|
78
|
+
end_date: datetime = field()
|
79
|
+
runner: CronRunner = field()
|
78
80
|
type: ReleaseType = field(default=ReleaseType.DEFAULT)
|
79
81
|
|
80
82
|
def __repr__(self) -> str:
|
83
|
+
"""Represent string"""
|
81
84
|
return repr(f"{self.date:%Y-%m-%d %H:%M:%S}")
|
82
85
|
|
83
86
|
def __str__(self) -> str:
|
87
|
+
"""Override string value of this release object with the date field.
|
88
|
+
|
89
|
+
:rtype: str
|
90
|
+
"""
|
84
91
|
return f"{self.date:%Y-%m-%d %H:%M:%S}"
|
85
92
|
|
86
93
|
@classmethod
|
87
94
|
def from_dt(cls, dt: datetime | str) -> Self:
|
88
95
|
"""Construct Release via datetime object only.
|
89
96
|
|
90
|
-
:param dt: A datetime object
|
97
|
+
:param dt: (datetime | str) A datetime object or string that want to
|
98
|
+
construct to the Release object.
|
91
99
|
|
92
|
-
:
|
100
|
+
:raise TypeError: If the type of the dt argument does not valid with
|
101
|
+
datetime or str object.
|
102
|
+
|
103
|
+
:rtype: Release
|
93
104
|
"""
|
94
105
|
if isinstance(dt, str):
|
95
106
|
dt: datetime = datetime.fromisoformat(dt)
|
96
107
|
elif not isinstance(dt, datetime):
|
97
108
|
raise TypeError(
|
98
|
-
"The `from_dt` need argument type be str or datetime
|
109
|
+
"The `from_dt` need the `dt` argument type be str or datetime "
|
110
|
+
"only."
|
99
111
|
)
|
100
112
|
|
101
113
|
return cls(
|
@@ -108,6 +120,8 @@ class Release:
|
|
108
120
|
def __eq__(self, other: Release | datetime) -> bool:
|
109
121
|
"""Override equal property that will compare only the same type or
|
110
122
|
datetime.
|
123
|
+
|
124
|
+
:rtype: bool
|
111
125
|
"""
|
112
126
|
if isinstance(other, self.__class__):
|
113
127
|
return self.date == other.date
|
@@ -118,6 +132,8 @@ class Release:
|
|
118
132
|
def __lt__(self, other: Release | datetime) -> bool:
|
119
133
|
"""Override equal property that will compare only the same type or
|
120
134
|
datetime.
|
135
|
+
|
136
|
+
:rtype: bool
|
121
137
|
"""
|
122
138
|
if isinstance(other, self.__class__):
|
123
139
|
return self.date < other.date
|
@@ -143,7 +159,7 @@ class ReleaseQueue:
|
|
143
159
|
|
144
160
|
:raise TypeError: If the type of input queue does not valid.
|
145
161
|
|
146
|
-
:rtype:
|
162
|
+
:rtype: ReleaseQueue
|
147
163
|
"""
|
148
164
|
if queue is None:
|
149
165
|
return cls()
|
@@ -174,7 +190,7 @@ class ReleaseQueue:
|
|
174
190
|
"""Check an input Release object is the first value of the
|
175
191
|
waiting queue.
|
176
192
|
|
177
|
-
:rtype:
|
193
|
+
:rtype: Release
|
178
194
|
"""
|
179
195
|
return self.queue[0]
|
180
196
|
|
@@ -265,12 +281,12 @@ class Workflow(BaseModel):
|
|
265
281
|
an input workflow name. The loader object will use this workflow name to
|
266
282
|
searching configuration data of this workflow model in conf path.
|
267
283
|
|
268
|
-
:raise ValueError: If the type does not match with current object.
|
269
|
-
|
270
284
|
:param name: A workflow name that want to pass to Loader object.
|
271
285
|
:param externals: An external parameters that want to pass to Loader
|
272
286
|
object.
|
273
287
|
|
288
|
+
:raise ValueError: If the type does not match with current object.
|
289
|
+
|
274
290
|
:rtype: Self
|
275
291
|
"""
|
276
292
|
loader: Loader = Loader(name, externals=(externals or {}))
|
@@ -285,11 +301,11 @@ class Workflow(BaseModel):
|
|
285
301
|
loader_data["name"] = name.replace(" ", "_")
|
286
302
|
|
287
303
|
# NOTE: Prepare `on` data
|
288
|
-
cls.
|
304
|
+
cls.__bypass_on__(loader_data, externals=externals)
|
289
305
|
return cls.model_validate(obj=loader_data)
|
290
306
|
|
291
307
|
@classmethod
|
292
|
-
def
|
308
|
+
def __bypass_on__(
|
293
309
|
cls,
|
294
310
|
data: DictData,
|
295
311
|
externals: DictData | None = None,
|
@@ -405,10 +421,13 @@ class Workflow(BaseModel):
|
|
405
421
|
return self
|
406
422
|
|
407
423
|
def job(self, name: str) -> Job:
|
408
|
-
"""Return
|
424
|
+
"""Return the workflow's job model that searching with an input job's
|
425
|
+
name or job's ID.
|
409
426
|
|
410
|
-
:param name: A job name that want to get from a mapping of
|
411
|
-
|
427
|
+
:param name: (str) A job name or ID that want to get from a mapping of
|
428
|
+
job models.
|
429
|
+
|
430
|
+
:raise ValueError: If a name or ID does not exist on the jobs field.
|
412
431
|
|
413
432
|
:rtype: Job
|
414
433
|
:return: A job model that exists on this workflow by input name.
|
@@ -505,18 +524,19 @@ class Workflow(BaseModel):
|
|
505
524
|
:param result: (Result) A result object for keeping context and status
|
506
525
|
data.
|
507
526
|
|
527
|
+
:raise TypeError: If a queue parameter does not match with ReleaseQueue
|
528
|
+
type.
|
529
|
+
|
508
530
|
:rtype: Result
|
509
531
|
"""
|
510
532
|
audit: type[Audit] = audit or get_audit()
|
511
533
|
name: str = override_log_name or self.name
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
elif parent_run_id: # pragma: no cov
|
519
|
-
result.set_parent_run_id(parent_run_id)
|
534
|
+
result: Result = Result.construct_with_rs_or_id(
|
535
|
+
result,
|
536
|
+
run_id=run_id,
|
537
|
+
parent_run_id=parent_run_id,
|
538
|
+
id_logic=name,
|
539
|
+
)
|
520
540
|
|
521
541
|
if queue is not None and not isinstance(queue, ReleaseQueue):
|
522
542
|
raise TypeError(
|
@@ -795,9 +815,7 @@ class Workflow(BaseModel):
|
|
795
815
|
partial_queue(q)
|
796
816
|
continue
|
797
817
|
|
798
|
-
# NOTE: Push the latest Release to the running queue.
|
799
818
|
heappush(q.running, release)
|
800
|
-
|
801
819
|
futures.append(
|
802
820
|
executor.submit(
|
803
821
|
self.release,
|
@@ -827,6 +845,7 @@ class Workflow(BaseModel):
|
|
827
845
|
params: DictData,
|
828
846
|
*,
|
829
847
|
result: Result | None = None,
|
848
|
+
event: Event | None = None,
|
830
849
|
raise_error: bool = True,
|
831
850
|
) -> Result:
|
832
851
|
"""Job execution with passing dynamic parameters from the main workflow
|
@@ -842,10 +861,12 @@ class Workflow(BaseModel):
|
|
842
861
|
|
843
862
|
:param job_id: A job ID that want to execute.
|
844
863
|
:param params: A params that was parameterized from workflow execution.
|
845
|
-
:param raise_error: A flag that raise error instead catching to result
|
846
|
-
if it gets exception from job execution.
|
847
864
|
:param result: (Result) A result object for keeping context and status
|
848
865
|
data.
|
866
|
+
:param event: (Event) An event manager that pass to the
|
867
|
+
PoolThreadExecutor.
|
868
|
+
:param raise_error: A flag that raise error instead catching to result
|
869
|
+
if it gets exception from job execution.
|
849
870
|
|
850
871
|
:rtype: Result
|
851
872
|
:return: Return the result object that receive the job execution result
|
@@ -863,6 +884,12 @@ class Workflow(BaseModel):
|
|
863
884
|
|
864
885
|
result.trace.info(f"[WORKFLOW]: Start execute job: {job_id!r}")
|
865
886
|
|
887
|
+
if event and event.is_set(): # pragma: no cov
|
888
|
+
raise WorkflowException(
|
889
|
+
"Workflow job was canceled from event that had set before "
|
890
|
+
"job execution."
|
891
|
+
)
|
892
|
+
|
866
893
|
# IMPORTANT:
|
867
894
|
# This execution change all job running IDs to the current workflow
|
868
895
|
# running ID, but it still trac log to the same parent running ID
|
@@ -876,6 +903,7 @@ class Workflow(BaseModel):
|
|
876
903
|
params=params,
|
877
904
|
run_id=result.run_id,
|
878
905
|
parent_run_id=result.parent_run_id,
|
906
|
+
event=event,
|
879
907
|
).context,
|
880
908
|
to=params,
|
881
909
|
)
|
@@ -889,7 +917,7 @@ class Workflow(BaseModel):
|
|
889
917
|
"Handle error from the job execution does not support yet."
|
890
918
|
) from None
|
891
919
|
|
892
|
-
return result.catch(status=
|
920
|
+
return result.catch(status=Status.SUCCESS, context=params)
|
893
921
|
|
894
922
|
def execute(
|
895
923
|
self,
|
@@ -927,22 +955,21 @@ class Workflow(BaseModel):
|
|
927
955
|
:rtype: Result
|
928
956
|
"""
|
929
957
|
ts: float = time.monotonic()
|
930
|
-
|
931
|
-
result
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
result.set_parent_run_id(parent_run_id)
|
958
|
+
result: Result = Result.construct_with_rs_or_id(
|
959
|
+
result,
|
960
|
+
run_id=run_id,
|
961
|
+
parent_run_id=parent_run_id,
|
962
|
+
id_logic=self.name,
|
963
|
+
)
|
937
964
|
|
938
965
|
result.trace.info(f"[WORKFLOW]: Start Execute: {self.name!r} ...")
|
939
966
|
|
940
967
|
# NOTE: It should not do anything if it does not have job.
|
941
968
|
if not self.jobs:
|
942
969
|
result.trace.warning(
|
943
|
-
f"[WORKFLOW]:
|
970
|
+
f"[WORKFLOW]: {self.name!r} does not have any jobs"
|
944
971
|
)
|
945
|
-
return result.catch(status=
|
972
|
+
return result.catch(status=Status.SUCCESS, context=params)
|
946
973
|
|
947
974
|
# NOTE: Create a job queue that keep the job that want to run after
|
948
975
|
# its dependency condition.
|
@@ -959,7 +986,7 @@ class Workflow(BaseModel):
|
|
959
986
|
# }
|
960
987
|
#
|
961
988
|
context: DictData = self.parameterize(params)
|
962
|
-
status:
|
989
|
+
status: Status = Status.SUCCESS
|
963
990
|
try:
|
964
991
|
if config.max_job_parallel == 1:
|
965
992
|
self.__exec_non_threading(
|
@@ -978,16 +1005,9 @@ class Workflow(BaseModel):
|
|
978
1005
|
timeout=timeout,
|
979
1006
|
)
|
980
1007
|
except WorkflowException as err:
|
981
|
-
status
|
982
|
-
context.update(
|
983
|
-
|
984
|
-
"errors": {
|
985
|
-
"class": err,
|
986
|
-
"name": err.__class__.__name__,
|
987
|
-
"message": f"{err.__class__.__name__}: {err}",
|
988
|
-
},
|
989
|
-
},
|
990
|
-
)
|
1008
|
+
status = Status.FAILED
|
1009
|
+
context.update({"errors": err.to_dict()})
|
1010
|
+
|
991
1011
|
return result.catch(status=status, context=context)
|
992
1012
|
|
993
1013
|
def __exec_threading(
|
@@ -1005,18 +1025,19 @@ class Workflow(BaseModel):
|
|
1005
1025
|
If a job need dependency, it will check dependency job ID from
|
1006
1026
|
context data before allow it run.
|
1007
1027
|
|
1008
|
-
:param result: A result model.
|
1028
|
+
:param result: (Result) A result model.
|
1009
1029
|
:param context: A context workflow data that want to downstream passing.
|
1010
1030
|
:param ts: A start timestamp that use for checking execute time should
|
1011
1031
|
time out.
|
1012
|
-
:param job_queue: A job queue object.
|
1013
|
-
:param timeout: A second value unit that bounding running time.
|
1032
|
+
:param job_queue: (Queue) A job queue object.
|
1033
|
+
:param timeout: (int) A second value unit that bounding running time.
|
1014
1034
|
:param thread_timeout: A timeout to waiting all futures complete.
|
1015
1035
|
|
1016
1036
|
:rtype: DictData
|
1017
1037
|
"""
|
1018
1038
|
not_timeout_flag: bool = True
|
1019
1039
|
timeout: int = timeout or config.max_job_exec_timeout
|
1040
|
+
event: Event = Event()
|
1020
1041
|
result.trace.debug(f"[WORKFLOW]: Run {self.name!r} with threading.")
|
1021
1042
|
|
1022
1043
|
# IMPORTANT: The job execution can run parallel and waiting by
|
@@ -1036,7 +1057,7 @@ class Workflow(BaseModel):
|
|
1036
1057
|
if not job.check_needs(context["jobs"]):
|
1037
1058
|
job_queue.task_done()
|
1038
1059
|
job_queue.put(job_id)
|
1039
|
-
time.sleep(0.
|
1060
|
+
time.sleep(0.15)
|
1040
1061
|
continue
|
1041
1062
|
|
1042
1063
|
# NOTE: Start workflow job execution with deep copy context data
|
@@ -1055,6 +1076,7 @@ class Workflow(BaseModel):
|
|
1055
1076
|
job_id=job_id,
|
1056
1077
|
params=context,
|
1057
1078
|
result=result,
|
1079
|
+
event=event,
|
1058
1080
|
),
|
1059
1081
|
)
|
1060
1082
|
|
@@ -1077,11 +1099,13 @@ class Workflow(BaseModel):
|
|
1077
1099
|
|
1078
1100
|
return context
|
1079
1101
|
|
1102
|
+
result.trace.error(
|
1103
|
+
f"[WORKFLOW]: Execution: {self.name!r} was timeout."
|
1104
|
+
)
|
1105
|
+
event.set()
|
1080
1106
|
for future in futures:
|
1081
1107
|
future.cancel()
|
1082
1108
|
|
1083
|
-
# NOTE: Raise timeout error.
|
1084
|
-
result.trace.error(f"[WORKFLOW]: Execution: {self.name!r} was timeout.")
|
1085
1109
|
raise WorkflowException(f"Execution: {self.name!r} was timeout.")
|
1086
1110
|
|
1087
1111
|
def __exec_non_threading(
|
@@ -1099,18 +1123,25 @@ class Workflow(BaseModel):
|
|
1099
1123
|
If a job need dependency, it will check dependency job ID from
|
1100
1124
|
context data before allow it run.
|
1101
1125
|
|
1102
|
-
:param result: A result model.
|
1126
|
+
:param result: (Result) A result model.
|
1103
1127
|
:param context: A context workflow data that want to downstream passing.
|
1104
|
-
:param ts: A start timestamp that use for checking execute time
|
1105
|
-
time out.
|
1106
|
-
:param timeout: A second value unit that bounding running time.
|
1128
|
+
:param ts: (float) A start timestamp that use for checking execute time
|
1129
|
+
should time out.
|
1130
|
+
:param timeout: (int) A second value unit that bounding running time.
|
1107
1131
|
|
1108
1132
|
:rtype: DictData
|
1109
1133
|
"""
|
1110
1134
|
not_timeout_flag: bool = True
|
1111
1135
|
timeout: int = timeout or config.max_job_exec_timeout
|
1136
|
+
event: Event = Event()
|
1137
|
+
future: Future | None = None
|
1112
1138
|
result.trace.debug(f"[WORKFLOW]: Run {self.name!r} with non-threading.")
|
1113
1139
|
|
1140
|
+
executor = ThreadPoolExecutor(
|
1141
|
+
max_workers=1,
|
1142
|
+
thread_name_prefix="wf_exec_non_threading_",
|
1143
|
+
)
|
1144
|
+
|
1114
1145
|
while not job_queue.empty() and (
|
1115
1146
|
not_timeout_flag := ((time.monotonic() - ts) < timeout)
|
1116
1147
|
):
|
@@ -1132,7 +1163,32 @@ class Workflow(BaseModel):
|
|
1132
1163
|
# 'params': <input-params>,
|
1133
1164
|
# 'jobs': {},
|
1134
1165
|
# }
|
1135
|
-
|
1166
|
+
if future is None:
|
1167
|
+
future: Future = executor.submit(
|
1168
|
+
self.execute_job,
|
1169
|
+
job_id=job_id,
|
1170
|
+
params=context,
|
1171
|
+
result=result,
|
1172
|
+
event=event,
|
1173
|
+
)
|
1174
|
+
result.trace.debug(f"[WORKFLOW]: Make future: {future}")
|
1175
|
+
time.sleep(0.025)
|
1176
|
+
elif future.done():
|
1177
|
+
if err := future.exception():
|
1178
|
+
result.trace.error(f"[WORKFLOW]: {err}")
|
1179
|
+
raise WorkflowException(str(err))
|
1180
|
+
|
1181
|
+
future = None
|
1182
|
+
job_queue.put(job_id)
|
1183
|
+
elif future.running():
|
1184
|
+
time.sleep(0.075)
|
1185
|
+
job_queue.put(job_id)
|
1186
|
+
else: # pragma: no cov
|
1187
|
+
job_queue.put(job_id)
|
1188
|
+
result.trace.debug(
|
1189
|
+
f"Execution non-threading does not handle case: {future} "
|
1190
|
+
f"that not running."
|
1191
|
+
)
|
1136
1192
|
|
1137
1193
|
# NOTE: Mark this job queue done.
|
1138
1194
|
job_queue.task_done()
|
@@ -1142,11 +1198,12 @@ class Workflow(BaseModel):
|
|
1142
1198
|
# NOTE: Wait for all items to finish processing by `task_done()`
|
1143
1199
|
# method.
|
1144
1200
|
job_queue.join()
|
1145
|
-
|
1201
|
+
executor.shutdown()
|
1146
1202
|
return context
|
1147
1203
|
|
1148
|
-
# NOTE: Raise timeout error.
|
1149
1204
|
result.trace.error(f"[WORKFLOW]: Execution: {self.name!r} was timeout.")
|
1205
|
+
event.set()
|
1206
|
+
executor.shutdown()
|
1150
1207
|
raise WorkflowException(f"Execution: {self.name!r} was timeout.")
|
1151
1208
|
|
1152
1209
|
|
@@ -1162,9 +1219,9 @@ class WorkflowTask:
|
|
1162
1219
|
arguments before passing to the parent release method.
|
1163
1220
|
"""
|
1164
1221
|
|
1165
|
-
alias: str
|
1166
|
-
workflow: Workflow
|
1167
|
-
runner: CronRunner
|
1222
|
+
alias: str = field()
|
1223
|
+
workflow: Workflow = field()
|
1224
|
+
runner: CronRunner = field()
|
1168
1225
|
values: DictData = field(default_factory=dict)
|
1169
1226
|
|
1170
1227
|
def release(
|
@@ -1185,6 +1242,11 @@ class WorkflowTask:
|
|
1185
1242
|
:param audit: An audit class that want to save the execution result.
|
1186
1243
|
:param queue: A ReleaseQueue object that use to mark complete.
|
1187
1244
|
|
1245
|
+
:raise ValueError: If a queue parameter does not pass while release
|
1246
|
+
is None.
|
1247
|
+
:raise TypeError: If a queue parameter does not match with ReleaseQueue
|
1248
|
+
type.
|
1249
|
+
|
1188
1250
|
:rtype: Result
|
1189
1251
|
"""
|
1190
1252
|
audit: type[Audit] = audit or get_audit()
|
@@ -1209,7 +1271,6 @@ class WorkflowTask:
|
|
1209
1271
|
else:
|
1210
1272
|
release = self.runner.date
|
1211
1273
|
|
1212
|
-
# NOTE: Call the workflow release method.
|
1213
1274
|
return self.workflow.release(
|
1214
1275
|
release=release,
|
1215
1276
|
params=self.values,
|
@@ -1264,13 +1325,14 @@ class WorkflowTask:
|
|
1264
1325
|
if self.runner.date > end_date:
|
1265
1326
|
return queue
|
1266
1327
|
|
1267
|
-
# NOTE: Push the Release object to queue.
|
1268
1328
|
heappush(queue.queue, workflow_release)
|
1269
|
-
|
1270
1329
|
return queue
|
1271
1330
|
|
1272
1331
|
def __repr__(self) -> str:
|
1273
|
-
"""Override the `__repr__` method.
|
1332
|
+
"""Override the `__repr__` method.
|
1333
|
+
|
1334
|
+
:rtype: str
|
1335
|
+
"""
|
1274
1336
|
return (
|
1275
1337
|
f"{self.__class__.__name__}(alias={self.alias!r}, "
|
1276
1338
|
f"workflow={self.workflow.name!r}, runner={self.runner!r}, "
|
@@ -1278,7 +1340,10 @@ class WorkflowTask:
|
|
1278
1340
|
)
|
1279
1341
|
|
1280
1342
|
def __eq__(self, other: WorkflowTask) -> bool:
|
1281
|
-
"""Override equal property that will compare only the same type.
|
1343
|
+
"""Override the equal property that will compare only the same type.
|
1344
|
+
|
1345
|
+
:rtype: bool
|
1346
|
+
"""
|
1282
1347
|
if isinstance(other, WorkflowTask):
|
1283
1348
|
return (
|
1284
1349
|
self.workflow.name == other.workflow.name
|
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.38
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -31,6 +31,10 @@ Provides-Extra: api
|
|
31
31
|
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
|
32
32
|
Requires-Dist: httpx; extra == "api"
|
33
33
|
Requires-Dist: ujson; extra == "api"
|
34
|
+
Provides-Extra: async
|
35
|
+
Requires-Dist: aiofiles; extra == "async"
|
36
|
+
Requires-Dist: aiohttp; extra == "async"
|
37
|
+
Dynamic: license-file
|
34
38
|
|
35
39
|
# Workflow Orchestration
|
36
40
|
|
@@ -61,10 +65,10 @@ configuration. It called **Metadata Driven Data Workflow**.
|
|
61
65
|
|
62
66
|
**:pushpin: <u>Rules of This Workflow engine</u>**:
|
63
67
|
|
64
|
-
1. The Minimum frequency unit of scheduling is **1
|
65
|
-
2. Can not re-run only failed stage and its pending downstream
|
66
|
-
3. All parallel tasks inside workflow engine use Multi-Threading
|
67
|
-
(
|
68
|
+
1. The Minimum frequency unit of scheduling is **1 Minute** 🕘
|
69
|
+
2. **Can not** re-run only failed stage and its pending downstream ↩️
|
70
|
+
3. All parallel tasks inside workflow engine use **Multi-Threading**
|
71
|
+
(Python 3.13 unlock GIL 🐍🔓)
|
68
72
|
|
69
73
|
---
|
70
74
|
|
@@ -165,6 +169,8 @@ run-py-local:
|
|
165
169
|
run-date: datetime
|
166
170
|
jobs:
|
167
171
|
getting-api-data:
|
172
|
+
runs-on:
|
173
|
+
type: local
|
168
174
|
stages:
|
169
175
|
- name: "Retrieve API Data"
|
170
176
|
id: retrieve-api
|