ddeutil-workflow 0.0.32__py3-none-any.whl → 0.0.34__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 +20 -12
- ddeutil/workflow/api/api.py +2 -2
- ddeutil/workflow/api/route.py +4 -3
- ddeutil/workflow/audit.py +252 -0
- ddeutil/workflow/{hook.py → call.py} +27 -27
- ddeutil/workflow/conf.py +163 -271
- ddeutil/workflow/job.py +113 -144
- ddeutil/workflow/result.py +199 -46
- ddeutil/workflow/scheduler.py +167 -151
- ddeutil/workflow/{stage.py → stages.py} +174 -89
- ddeutil/workflow/utils.py +20 -2
- ddeutil/workflow/workflow.py +172 -148
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.34.dist-info}/METADATA +43 -38
- ddeutil_workflow-0.0.34.dist-info/RECORD +26 -0
- ddeutil_workflow-0.0.32.dist-info/RECORD +0 -25
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.34.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.34.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.34.dist-info}/top_level.txt +0 -0
@@ -42,10 +42,10 @@ from pydantic.functional_validators import model_validator
|
|
42
42
|
from typing_extensions import Self
|
43
43
|
|
44
44
|
from .__types import DictData, DictStr, TupleStr
|
45
|
+
from .call import TagFunc, extract_call
|
45
46
|
from .conf import config, get_logger
|
46
47
|
from .exceptions import StageException
|
47
|
-
from .
|
48
|
-
from .result import Result
|
48
|
+
from .result import Result, Status
|
49
49
|
from .templates import not_in_template, param2template
|
50
50
|
from .utils import (
|
51
51
|
cut_id,
|
@@ -60,7 +60,7 @@ __all__: TupleStr = (
|
|
60
60
|
"EmptyStage",
|
61
61
|
"BashStage",
|
62
62
|
"PyStage",
|
63
|
-
"
|
63
|
+
"CallStage",
|
64
64
|
"TriggerStage",
|
65
65
|
"Stage",
|
66
66
|
)
|
@@ -100,7 +100,7 @@ class BaseStage(BaseModel, ABC):
|
|
100
100
|
return self.id or self.name
|
101
101
|
|
102
102
|
@model_validator(mode="after")
|
103
|
-
def
|
103
|
+
def __prepare_running_id(self) -> Self:
|
104
104
|
"""Prepare stage running ID that use default value of field and this
|
105
105
|
method will validate name and id fields should not contain any template
|
106
106
|
parameter (exclude matrix template).
|
@@ -121,36 +121,45 @@ class BaseStage(BaseModel, ABC):
|
|
121
121
|
return self
|
122
122
|
|
123
123
|
@abstractmethod
|
124
|
-
def execute(
|
124
|
+
def execute(
|
125
|
+
self, params: DictData, *, result: Result | None = None
|
126
|
+
) -> Result:
|
125
127
|
"""Execute abstraction method that action something by sub-model class.
|
126
128
|
This is important method that make this class is able to be the stage.
|
127
129
|
|
128
130
|
:param params: A parameter data that want to use in this execution.
|
129
|
-
:param
|
131
|
+
:param result: (Result) A result object for keeping context and status
|
132
|
+
data.
|
130
133
|
|
131
134
|
:rtype: Result
|
132
135
|
"""
|
133
136
|
raise NotImplementedError("Stage should implement ``execute`` method.")
|
134
137
|
|
135
138
|
def handler_execute(
|
136
|
-
self,
|
139
|
+
self,
|
140
|
+
params: DictData,
|
141
|
+
*,
|
142
|
+
run_id: str | None = None,
|
143
|
+
parent_run_id: str | None = None,
|
144
|
+
result: Result | None = None,
|
137
145
|
) -> Result:
|
138
|
-
"""Handler result from the stage
|
146
|
+
"""Handler execution result from the stage `execute` method.
|
139
147
|
|
140
148
|
This stage exception handler still use ok-error concept, but it
|
141
149
|
allows you force catching an output result with error message by
|
142
150
|
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
|
143
151
|
|
144
152
|
Execution --> Ok --> Result
|
145
|
-
|-status:
|
153
|
+
|-status: Status.SUCCESS
|
146
154
|
|-context:
|
147
155
|
|-outputs: ...
|
148
156
|
|
149
157
|
--> Error --> Result (if env var was set)
|
150
|
-
|-status:
|
151
|
-
|-
|
152
|
-
|-
|
153
|
-
|-
|
158
|
+
|-status: Status.FAILED
|
159
|
+
|-errors:
|
160
|
+
|-class: ...
|
161
|
+
|-name: ...
|
162
|
+
|-message: ...
|
154
163
|
|
155
164
|
--> Error --> Raise StageException(...)
|
156
165
|
|
@@ -158,31 +167,35 @@ class BaseStage(BaseModel, ABC):
|
|
158
167
|
from current stage ID before release the final result.
|
159
168
|
|
160
169
|
:param params: A parameter data that want to use in this execution.
|
161
|
-
:param run_id: A running stage ID for this execution.
|
170
|
+
:param run_id: (str) A running stage ID for this execution.
|
171
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
172
|
+
:param result: (Result) A result object for keeping context and status
|
173
|
+
data.
|
162
174
|
|
163
175
|
:rtype: Result
|
164
176
|
"""
|
165
|
-
if
|
166
|
-
|
177
|
+
if result is None:
|
178
|
+
result: Result = Result(
|
179
|
+
run_id=(
|
180
|
+
run_id or gen_id(self.name + (self.id or ""), unique=True)
|
181
|
+
),
|
182
|
+
parent_run_id=parent_run_id,
|
183
|
+
)
|
184
|
+
elif parent_run_id:
|
185
|
+
result.set_parent_run_id(parent_run_id)
|
167
186
|
|
168
|
-
rs_raise: Result = Result(status=1, run_id=run_id)
|
169
187
|
try:
|
170
|
-
|
171
|
-
return self.execute(params, run_id=run_id)
|
188
|
+
return self.execute(params, result=result)
|
172
189
|
except Exception as err:
|
173
|
-
|
174
|
-
|
175
|
-
f"({cut_id(run_id)}) [STAGE]: {err.__class__.__name__}: "
|
176
|
-
f"{err}"
|
177
|
-
)
|
190
|
+
result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
|
191
|
+
|
178
192
|
if config.stage_raise_error:
|
179
193
|
# NOTE: If error that raise from stage execution course by
|
180
194
|
# itself, it will return that error with previous
|
181
195
|
# dependency.
|
182
196
|
if isinstance(err, StageException):
|
183
|
-
raise
|
184
|
-
|
185
|
-
) from err
|
197
|
+
raise
|
198
|
+
|
186
199
|
raise StageException(
|
187
200
|
f"{self.__class__.__name__}: \n\t"
|
188
201
|
f"{err.__class__.__name__}: {err}"
|
@@ -190,11 +203,14 @@ class BaseStage(BaseModel, ABC):
|
|
190
203
|
|
191
204
|
# NOTE: Catching exception error object to result with
|
192
205
|
# error_message and error keys.
|
193
|
-
return
|
194
|
-
status=
|
206
|
+
return result.catch(
|
207
|
+
status=Status.FAILED,
|
195
208
|
context={
|
196
|
-
"
|
197
|
-
|
209
|
+
"errors": {
|
210
|
+
"class": err,
|
211
|
+
"name": err.__class__.__name__,
|
212
|
+
"message": f"{err.__class__.__name__}: {err}",
|
213
|
+
},
|
198
214
|
},
|
199
215
|
)
|
200
216
|
|
@@ -238,8 +254,12 @@ class BaseStage(BaseModel, ABC):
|
|
238
254
|
else gen_id(param2template(self.name, params=to))
|
239
255
|
)
|
240
256
|
|
257
|
+
errors: DictData = (
|
258
|
+
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
259
|
+
)
|
260
|
+
|
241
261
|
# NOTE: Set the output to that stage generated ID with ``outputs`` key.
|
242
|
-
to["stages"][_id] = {"outputs": output}
|
262
|
+
to["stages"][_id] = {"outputs": output, **errors}
|
243
263
|
return to
|
244
264
|
|
245
265
|
def is_skipped(self, params: DictData | None = None) -> bool:
|
@@ -295,7 +315,9 @@ class EmptyStage(BaseStage):
|
|
295
315
|
ge=0,
|
296
316
|
)
|
297
317
|
|
298
|
-
def execute(
|
318
|
+
def execute(
|
319
|
+
self, params: DictData, *, result: Result | None = None
|
320
|
+
) -> Result:
|
299
321
|
"""Execution method for the Empty stage that do only logging out to
|
300
322
|
stdout. This method does not use the `handler_result` decorator because
|
301
323
|
it does not get any error from logging function.
|
@@ -305,22 +327,26 @@ class EmptyStage(BaseStage):
|
|
305
327
|
|
306
328
|
:param params: A context data that want to add output result. But this
|
307
329
|
stage does not pass any output.
|
308
|
-
:param
|
330
|
+
:param result: (Result) A result object for keeping context and status
|
331
|
+
data.
|
309
332
|
|
310
333
|
:rtype: Result
|
311
334
|
"""
|
312
|
-
|
313
|
-
|
335
|
+
if result is None: # pragma: no cov
|
336
|
+
result: Result = Result(
|
337
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
338
|
+
)
|
339
|
+
|
340
|
+
result.trace.info(
|
341
|
+
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
314
342
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
315
343
|
)
|
316
344
|
if self.sleep > 0:
|
317
345
|
if self.sleep > 30:
|
318
|
-
|
319
|
-
f"({cut_id(run_id)}) [STAGE]: ... sleep "
|
320
|
-
f"({self.sleep} seconds)"
|
321
|
-
)
|
346
|
+
result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
|
322
347
|
time.sleep(self.sleep)
|
323
|
-
|
348
|
+
|
349
|
+
return result.catch(status=Status.SUCCESS)
|
324
350
|
|
325
351
|
|
326
352
|
class BashStage(BaseStage):
|
@@ -334,7 +360,7 @@ class BashStage(BaseStage):
|
|
334
360
|
|
335
361
|
Data Validate:
|
336
362
|
>>> stage = {
|
337
|
-
... "name": "Shell stage execution",
|
363
|
+
... "name": "The Shell stage execution",
|
338
364
|
... "bash": 'echo "Hello $FOO"',
|
339
365
|
... "env": {
|
340
366
|
... "FOO": "BAR",
|
@@ -391,20 +417,30 @@ class BashStage(BaseStage):
|
|
391
417
|
# Note: Remove .sh file that use to run bash.
|
392
418
|
Path(f"./{f_name}").unlink()
|
393
419
|
|
394
|
-
def execute(
|
420
|
+
def execute(
|
421
|
+
self, params: DictData, *, result: Result | None = None
|
422
|
+
) -> Result:
|
395
423
|
"""Execute the Bash statement with the Python build-in ``subprocess``
|
396
424
|
package.
|
397
425
|
|
398
426
|
:param params: A parameter data that want to use in this execution.
|
399
|
-
:param
|
427
|
+
:param result: (Result) A result object for keeping context and status
|
428
|
+
data.
|
400
429
|
|
401
430
|
:rtype: Result
|
402
431
|
"""
|
432
|
+
if result is None: # pragma: no cov
|
433
|
+
result: Result = Result(
|
434
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
435
|
+
)
|
436
|
+
|
403
437
|
bash: str = param2template(dedent(self.bash), params)
|
404
438
|
|
405
|
-
|
439
|
+
result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
|
406
440
|
with self.create_sh_file(
|
407
|
-
bash=bash,
|
441
|
+
bash=bash,
|
442
|
+
env=param2template(self.env, params),
|
443
|
+
run_id=result.run_id,
|
408
444
|
) as sh:
|
409
445
|
rs: CompletedProcess = subprocess.run(
|
410
446
|
sh, shell=False, capture_output=True, text=True
|
@@ -420,14 +456,13 @@ class BashStage(BaseStage):
|
|
420
456
|
f"Subprocess: {err}\nRunning Statement:\n---\n"
|
421
457
|
f"```bash\n{bash}\n```"
|
422
458
|
)
|
423
|
-
return
|
424
|
-
status=
|
459
|
+
return result.catch(
|
460
|
+
status=Status.SUCCESS,
|
425
461
|
context={
|
426
462
|
"return_code": rs.returncode,
|
427
463
|
"stdout": rs.stdout.rstrip("\n") or None,
|
428
464
|
"stderr": rs.stderr.rstrip("\n") or None,
|
429
465
|
},
|
430
|
-
run_id=run_id,
|
431
466
|
)
|
432
467
|
|
433
468
|
|
@@ -459,12 +494,24 @@ class PyStage(BaseStage):
|
|
459
494
|
)
|
460
495
|
|
461
496
|
@staticmethod
|
462
|
-
def
|
463
|
-
|
497
|
+
def filter_locals(values: DictData) -> Iterator[str]:
|
498
|
+
"""Filter a locals input values.
|
499
|
+
|
500
|
+
:param values: (DictData) A locals values that want to filter.
|
501
|
+
|
502
|
+
:rtype: Iterator[str]
|
503
|
+
"""
|
504
|
+
from inspect import isclass, ismodule
|
464
505
|
|
465
506
|
for value in values:
|
466
|
-
|
507
|
+
|
508
|
+
if (
|
509
|
+
value == "__annotations__"
|
510
|
+
or ismodule(values[value])
|
511
|
+
or isclass(values[value])
|
512
|
+
):
|
467
513
|
continue
|
514
|
+
|
468
515
|
yield value
|
469
516
|
|
470
517
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
@@ -480,7 +527,7 @@ class PyStage(BaseStage):
|
|
480
527
|
lc: DictData = output.get("locals", {})
|
481
528
|
super().set_outputs(
|
482
529
|
(
|
483
|
-
{k: lc[k] for k in self.
|
530
|
+
{k: lc[k] for k in self.filter_locals(lc)}
|
484
531
|
| {k: output[k] for k in output if k.startswith("error")}
|
485
532
|
),
|
486
533
|
to=to,
|
@@ -492,15 +539,23 @@ class PyStage(BaseStage):
|
|
492
539
|
to.update({k: gb[k] for k in to if k in gb})
|
493
540
|
return to
|
494
541
|
|
495
|
-
def execute(
|
542
|
+
def execute(
|
543
|
+
self, params: DictData, *, result: Result | None = None
|
544
|
+
) -> Result:
|
496
545
|
"""Execute the Python statement that pass all globals and input params
|
497
546
|
to globals argument on ``exec`` build-in function.
|
498
547
|
|
499
548
|
:param params: A parameter that want to pass before run any statement.
|
500
|
-
:param
|
549
|
+
:param result: (Result) A result object for keeping context and status
|
550
|
+
data.
|
501
551
|
|
502
552
|
:rtype: Result
|
503
553
|
"""
|
554
|
+
if result is None: # pragma: no cov
|
555
|
+
result: Result = Result(
|
556
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
557
|
+
)
|
558
|
+
|
504
559
|
# NOTE: Replace the run statement that has templating value.
|
505
560
|
run: str = param2template(dedent(self.run), params)
|
506
561
|
|
@@ -511,21 +566,19 @@ class PyStage(BaseStage):
|
|
511
566
|
lc: DictData = {}
|
512
567
|
|
513
568
|
# NOTE: Start exec the run statement.
|
514
|
-
|
569
|
+
result.trace.info(f"[STAGE]: Py-Execute: {self.name}")
|
515
570
|
|
516
571
|
# WARNING: The exec build-in function is very dangerous. So, it
|
517
572
|
# should use the re module to validate exec-string before running.
|
518
573
|
exec(run, _globals, lc)
|
519
574
|
|
520
|
-
return
|
521
|
-
status=
|
522
|
-
context={"locals": lc, "globals": _globals},
|
523
|
-
run_id=run_id,
|
575
|
+
return result.catch(
|
576
|
+
status=Status.SUCCESS, context={"locals": lc, "globals": _globals}
|
524
577
|
)
|
525
578
|
|
526
579
|
|
527
|
-
class
|
528
|
-
"""
|
580
|
+
class CallStage(BaseStage):
|
581
|
+
"""Call executor that call the Python function from registry with tag
|
529
582
|
decorator function in ``utils`` module and run it with input arguments.
|
530
583
|
|
531
584
|
This stage is different with PyStage because the PyStage is just calling
|
@@ -543,35 +596,43 @@ class HookStage(BaseStage):
|
|
543
596
|
|
544
597
|
uses: str = Field(
|
545
598
|
description=(
|
546
|
-
"A pointer that want to load function from the
|
599
|
+
"A pointer that want to load function from the call registry."
|
547
600
|
),
|
548
601
|
)
|
549
602
|
args: DictData = Field(
|
550
603
|
default_factory=dict,
|
551
|
-
description="An arguments that want to pass to the
|
604
|
+
description="An arguments that want to pass to the call function.",
|
552
605
|
alias="with",
|
553
606
|
)
|
554
607
|
|
555
|
-
def execute(
|
556
|
-
|
608
|
+
def execute(
|
609
|
+
self, params: DictData, *, result: Result | None = None
|
610
|
+
) -> Result:
|
611
|
+
"""Execute the Call function that already in the call registry.
|
557
612
|
|
558
|
-
:raise ValueError: When the necessary arguments of
|
613
|
+
:raise ValueError: When the necessary arguments of call function do not
|
559
614
|
set from the input params argument.
|
560
|
-
:raise TypeError: When the return type of
|
615
|
+
:raise TypeError: When the return type of call function does not be
|
561
616
|
dict type.
|
562
617
|
|
563
618
|
:param params: A parameter that want to pass before run any statement.
|
564
619
|
:type params: DictData
|
565
|
-
:param
|
620
|
+
:param result: (Result) A result object for keeping context and status
|
621
|
+
data.
|
566
622
|
:type: str | None
|
567
623
|
|
568
624
|
:rtype: Result
|
569
625
|
"""
|
570
|
-
|
626
|
+
if result is None: # pragma: no cov
|
627
|
+
result: Result = Result(
|
628
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
629
|
+
)
|
630
|
+
|
631
|
+
t_func: TagFunc = extract_call(param2template(self.uses, params))()
|
571
632
|
|
572
633
|
# VALIDATE: check input task caller parameters that exists before
|
573
634
|
# calling.
|
574
|
-
args: DictData = param2template(self.args, params)
|
635
|
+
args: DictData = {"result": result} | param2template(self.args, params)
|
575
636
|
ips = inspect.signature(t_func)
|
576
637
|
if any(
|
577
638
|
(k.removeprefix("_") not in args and k not in args)
|
@@ -587,20 +648,20 @@ class HookStage(BaseStage):
|
|
587
648
|
if k.removeprefix("_") in args:
|
588
649
|
args[k] = args.pop(k.removeprefix("_"))
|
589
650
|
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
)
|
651
|
+
if "result" not in ips.parameters:
|
652
|
+
args.pop("result")
|
653
|
+
|
654
|
+
result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
|
594
655
|
rs: DictData = t_func(**param2template(args, params))
|
595
656
|
|
596
657
|
# VALIDATE:
|
597
|
-
# Check the result type from
|
658
|
+
# Check the result type from call function, it should be dict.
|
598
659
|
if not isinstance(rs, dict):
|
599
660
|
raise TypeError(
|
600
661
|
f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
|
601
662
|
f"to result model, you change return type to `dict`."
|
602
663
|
)
|
603
|
-
return
|
664
|
+
return result.catch(status=Status.SUCCESS, context=rs)
|
604
665
|
|
605
666
|
|
606
667
|
class TriggerStage(BaseStage):
|
@@ -626,31 +687,37 @@ class TriggerStage(BaseStage):
|
|
626
687
|
description="A parameter that want to pass to workflow execution.",
|
627
688
|
)
|
628
689
|
|
629
|
-
def execute(
|
690
|
+
def execute(
|
691
|
+
self, params: DictData, *, result: Result | None = None
|
692
|
+
) -> Result:
|
630
693
|
"""Trigger another workflow execution. It will wait the trigger
|
631
694
|
workflow running complete before catching its result.
|
632
695
|
|
633
696
|
:param params: A parameter data that want to use in this execution.
|
634
|
-
:param
|
697
|
+
:param result: (Result) A result object for keeping context and status
|
698
|
+
data.
|
635
699
|
|
636
700
|
:rtype: Result
|
637
701
|
"""
|
638
702
|
# NOTE: Lazy import this workflow object.
|
639
703
|
from . import Workflow
|
640
704
|
|
705
|
+
if result is None: # pragma: no cov
|
706
|
+
result: Result = Result(
|
707
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
708
|
+
)
|
709
|
+
|
641
710
|
# NOTE: Loading workflow object from trigger name.
|
642
711
|
_trigger: str = param2template(self.trigger, params=params)
|
643
712
|
|
644
713
|
# NOTE: Set running workflow ID from running stage ID to external
|
645
714
|
# params on Loader object.
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
)
|
650
|
-
return wf.execute(
|
715
|
+
workflow: Workflow = Workflow.from_loader(name=_trigger)
|
716
|
+
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
717
|
+
return workflow.execute(
|
651
718
|
params=param2template(self.params, params),
|
652
|
-
|
653
|
-
)
|
719
|
+
result=result,
|
720
|
+
)
|
654
721
|
|
655
722
|
|
656
723
|
# NOTE:
|
@@ -661,19 +728,37 @@ class TriggerStage(BaseStage):
|
|
661
728
|
Stage = Union[
|
662
729
|
PyStage,
|
663
730
|
BashStage,
|
664
|
-
|
731
|
+
CallStage,
|
665
732
|
TriggerStage,
|
666
733
|
EmptyStage,
|
667
734
|
]
|
668
735
|
|
669
736
|
|
670
737
|
# TODO: Not implement this stages yet
|
671
|
-
class ParallelStage(
|
738
|
+
class ParallelStage(BaseStage): # pragma: no cov
|
672
739
|
parallel: list[Stage]
|
673
740
|
max_parallel_core: int = Field(default=2)
|
674
741
|
|
742
|
+
def execute(
|
743
|
+
self, params: DictData, *, result: Result | None = None
|
744
|
+
) -> Result: ...
|
745
|
+
|
746
|
+
|
747
|
+
# TODO: Not implement this stages yet
|
748
|
+
class ForEachStage(BaseStage): # pragma: no cov
|
749
|
+
foreach: list[str]
|
750
|
+
stages: list[Stage]
|
751
|
+
|
752
|
+
def execute(
|
753
|
+
self, params: DictData, *, result: Result | None = None
|
754
|
+
) -> Result: ...
|
755
|
+
|
675
756
|
|
676
757
|
# TODO: Not implement this stages yet
|
677
|
-
class
|
758
|
+
class HookStage(BaseStage): # pragma: no cov
|
678
759
|
foreach: list[str]
|
679
760
|
stages: list[Stage]
|
761
|
+
|
762
|
+
def execute(
|
763
|
+
self, params: DictData, *, result: Result | None = None
|
764
|
+
) -> Result: ...
|
ddeutil/workflow/utils.py
CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
8
8
|
import logging
|
9
9
|
import stat
|
10
10
|
import time
|
11
|
-
from collections.abc import Iterator
|
11
|
+
from collections.abc import Iterator, Mapping
|
12
12
|
from datetime import datetime, timedelta
|
13
13
|
from hashlib import md5
|
14
14
|
from inspect import isfunction
|
@@ -199,7 +199,7 @@ def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
|
199
199
|
)
|
200
200
|
|
201
201
|
|
202
|
-
def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
202
|
+
def batch(iterable: Iterator[Any] | range, n: int) -> Iterator[Any]:
|
203
203
|
"""Batch data into iterators of length n. The last batch may be shorter.
|
204
204
|
|
205
205
|
Example:
|
@@ -240,3 +240,21 @@ def cut_id(run_id: str, *, num: int = 6) -> str:
|
|
240
240
|
:rtype: str
|
241
241
|
"""
|
242
242
|
return run_id[-num:]
|
243
|
+
|
244
|
+
|
245
|
+
def deep_update(origin: DictData, u: Mapping) -> DictData:
|
246
|
+
"""Deep update dict.
|
247
|
+
|
248
|
+
Example:
|
249
|
+
>>> deep_update(
|
250
|
+
... origin={"jobs": {"job01": "foo"}},
|
251
|
+
... u={"jobs": {"job02": "bar"}},
|
252
|
+
... )
|
253
|
+
{"jobs": {"job01": "foo", "job02": "bar"}}
|
254
|
+
"""
|
255
|
+
for k, value in u.items():
|
256
|
+
if isinstance(value, Mapping) and value:
|
257
|
+
origin[k] = deep_update(origin.get(k, {}), value)
|
258
|
+
else:
|
259
|
+
origin[k] = value
|
260
|
+
return origin
|