ddeutil-workflow 0.0.40__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__init__.py +21 -25
- ddeutil/workflow/api/api.py +8 -8
- ddeutil/workflow/api/routes/logs.py +1 -2
- ddeutil/workflow/api/routes/schedules.py +5 -5
- ddeutil/workflow/api/routes/workflows.py +3 -3
- ddeutil/workflow/conf.py +50 -29
- ddeutil/workflow/cron.py +12 -13
- ddeutil/workflow/job.py +68 -9
- ddeutil/workflow/logs.py +358 -69
- ddeutil/workflow/params.py +1 -0
- ddeutil/workflow/result.py +6 -15
- ddeutil/workflow/{templates.py → reusables.py} +199 -10
- ddeutil/workflow/scheduler.py +27 -29
- ddeutil/workflow/stages.py +423 -64
- ddeutil/workflow/utils.py +10 -0
- ddeutil/workflow/workflow.py +119 -74
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/METADATA +12 -9
- ddeutil_workflow-0.0.42.dist-info/RECORD +30 -0
- ddeutil/workflow/audit.py +0 -257
- ddeutil/workflow/caller.py +0 -179
- ddeutil/workflow/context.py +0 -61
- ddeutil_workflow-0.0.40.dist-info/RECORD +0 -33
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.40.dist-info → ddeutil_workflow-0.0.42.dist-info}/top_level.txt +0 -0
@@ -3,32 +3,36 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
# [x] Use dynamic config
|
7
|
+
"""Reusables module that keep any templating functions."""
|
6
8
|
from __future__ import annotations
|
7
9
|
|
8
10
|
import inspect
|
9
11
|
import logging
|
10
12
|
from ast import Call, Constant, Expr, Module, Name, parse
|
13
|
+
from dataclasses import dataclass
|
11
14
|
from datetime import datetime
|
12
15
|
from functools import wraps
|
13
16
|
from importlib import import_module
|
14
|
-
from typing import Any, Callable, Protocol, TypeVar, Union
|
17
|
+
from typing import Any, Callable, Optional, Protocol, TypeVar, Union
|
15
18
|
|
16
19
|
try:
|
17
20
|
from typing import ParamSpec
|
18
21
|
except ImportError:
|
19
22
|
from typing_extensions import ParamSpec
|
20
23
|
|
21
|
-
from ddeutil.core import getdot, import_string
|
24
|
+
from ddeutil.core import getdot, import_string, lazy
|
22
25
|
from ddeutil.io import search_env_replace
|
23
26
|
|
24
27
|
from .__types import DictData, Re
|
25
|
-
from .conf import
|
28
|
+
from .conf import dynamic
|
26
29
|
from .exceptions import UtilException
|
27
30
|
|
28
31
|
T = TypeVar("T")
|
29
32
|
P = ParamSpec("P")
|
30
33
|
|
31
34
|
logger = logging.getLogger("ddeutil.workflow")
|
35
|
+
logging.getLogger("asyncio").setLevel(logging.INFO)
|
32
36
|
|
33
37
|
|
34
38
|
FILTERS: dict[str, callable] = { # pragma: no cov
|
@@ -77,13 +81,17 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
|
|
77
81
|
return func_internal
|
78
82
|
|
79
83
|
|
80
|
-
def make_filter_registry(
|
84
|
+
def make_filter_registry(
|
85
|
+
registers: Optional[list[str]] = None,
|
86
|
+
) -> dict[str, FilterRegistry]:
|
81
87
|
"""Return registries of all functions that able to called with task.
|
82
88
|
|
89
|
+
:param registers: (Optional[list[str]]) Override list of register.
|
90
|
+
|
83
91
|
:rtype: dict[str, FilterRegistry]
|
84
92
|
"""
|
85
93
|
rs: dict[str, FilterRegistry] = {}
|
86
|
-
for module in
|
94
|
+
for module in dynamic("regis_filter", f=registers):
|
87
95
|
# NOTE: try to sequential import task functions
|
88
96
|
try:
|
89
97
|
importer = import_module(module)
|
@@ -251,6 +259,7 @@ def str2template(
|
|
251
259
|
params: DictData,
|
252
260
|
*,
|
253
261
|
filters: dict[str, FilterRegistry] | None = None,
|
262
|
+
registers: Optional[list[str]] = None,
|
254
263
|
) -> str:
|
255
264
|
"""(Sub-function) Pass param to template string that can search by
|
256
265
|
``RE_CALLER`` regular expression.
|
@@ -263,10 +272,13 @@ def str2template(
|
|
263
272
|
:param params: (DictData) A parameter value that getting with matched
|
264
273
|
regular expression.
|
265
274
|
:param filters: A mapping of filter registry.
|
275
|
+
:param registers: (Optional[list[str]]) Override list of register.
|
266
276
|
|
267
277
|
:rtype: str
|
268
278
|
"""
|
269
|
-
filters: dict[str, FilterRegistry] = filters or make_filter_registry(
|
279
|
+
filters: dict[str, FilterRegistry] = filters or make_filter_registry(
|
280
|
+
registers=registers
|
281
|
+
)
|
270
282
|
|
271
283
|
# NOTE: remove space before and after this string value.
|
272
284
|
value: str = value.strip()
|
@@ -315,6 +327,8 @@ def param2template(
|
|
315
327
|
value: T,
|
316
328
|
params: DictData,
|
317
329
|
filters: dict[str, FilterRegistry] | None = None,
|
330
|
+
*,
|
331
|
+
extras: Optional[DictData] = None,
|
318
332
|
) -> T:
|
319
333
|
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
320
334
|
expression.
|
@@ -323,18 +337,29 @@ def param2template(
|
|
323
337
|
:param params: A parameter value that getting with matched regular
|
324
338
|
expression.
|
325
339
|
:param filters: A filter mapping for mapping with `map_post_filter` func.
|
340
|
+
:param extras: (Optional[list[str]]) An Override extras.
|
326
341
|
|
327
342
|
:rtype: T
|
328
343
|
:returns: An any getter value from the params input.
|
329
344
|
"""
|
330
|
-
|
345
|
+
registers: Optional[list[str]] = (
|
346
|
+
extras.get("regis_filter") if extras else None
|
347
|
+
)
|
348
|
+
filters: dict[str, FilterRegistry] = filters or make_filter_registry(
|
349
|
+
registers=registers
|
350
|
+
)
|
331
351
|
if isinstance(value, dict):
|
332
|
-
return {
|
352
|
+
return {
|
353
|
+
k: param2template(value[k], params, filters, extras=extras)
|
354
|
+
for k in value
|
355
|
+
}
|
333
356
|
elif isinstance(value, (list, tuple, set)):
|
334
|
-
return type(value)(
|
357
|
+
return type(value)(
|
358
|
+
[param2template(i, params, filters, extras=extras) for i in value]
|
359
|
+
)
|
335
360
|
elif not isinstance(value, str):
|
336
361
|
return value
|
337
|
-
return str2template(value, params, filters=filters)
|
362
|
+
return str2template(value, params, filters=filters, registers=registers)
|
338
363
|
|
339
364
|
|
340
365
|
@custom_filter("fmt") # pragma: no cov
|
@@ -359,3 +384,167 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
359
384
|
def coalesce(value: T | None, default: Any) -> T:
|
360
385
|
"""Coalesce with default value if the main value is None."""
|
361
386
|
return default if value is None else value
|
387
|
+
|
388
|
+
|
389
|
+
class TagFunc(Protocol):
|
390
|
+
"""Tag Function Protocol"""
|
391
|
+
|
392
|
+
name: str
|
393
|
+
tag: str
|
394
|
+
|
395
|
+
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
396
|
+
|
397
|
+
|
398
|
+
ReturnTagFunc = Callable[P, TagFunc]
|
399
|
+
DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
|
400
|
+
|
401
|
+
|
402
|
+
def tag(
|
403
|
+
name: str, alias: str | None = None
|
404
|
+
) -> DecoratorTagFunc: # pragma: no cov
|
405
|
+
"""Tag decorator function that set function attributes, ``tag`` and ``name``
|
406
|
+
for making registries variable.
|
407
|
+
|
408
|
+
:param: name: (str) A tag name for make different use-case of a function.
|
409
|
+
:param: alias: (str) A alias function name that keeping in registries.
|
410
|
+
If this value does not supply, it will use original function name
|
411
|
+
from `__name__` argument.
|
412
|
+
|
413
|
+
:rtype: Callable[P, TagFunc]
|
414
|
+
"""
|
415
|
+
|
416
|
+
def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
|
417
|
+
func.tag = name
|
418
|
+
func.name = alias or func.__name__.replace("_", "-")
|
419
|
+
|
420
|
+
@wraps(func)
|
421
|
+
def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
|
422
|
+
"""Wrapped function."""
|
423
|
+
return func(*args, **kwargs)
|
424
|
+
|
425
|
+
@wraps(func)
|
426
|
+
async def async_wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
|
427
|
+
"""Wrapped async function."""
|
428
|
+
return await func(*args, **kwargs)
|
429
|
+
|
430
|
+
return async_wrapped if inspect.iscoroutinefunction(func) else wrapped
|
431
|
+
|
432
|
+
return func_internal
|
433
|
+
|
434
|
+
|
435
|
+
Registry = dict[str, Callable[[], TagFunc]]
|
436
|
+
|
437
|
+
|
438
|
+
def make_registry(
|
439
|
+
submodule: str,
|
440
|
+
registries: Optional[list[str]] = None,
|
441
|
+
) -> dict[str, Registry]:
|
442
|
+
"""Return registries of all functions that able to called with task.
|
443
|
+
|
444
|
+
:param submodule: (str) A module prefix that want to import registry.
|
445
|
+
:param registries: (Optional[list[str]]) A list of registry.
|
446
|
+
|
447
|
+
:rtype: dict[str, Registry]
|
448
|
+
"""
|
449
|
+
rs: dict[str, Registry] = {}
|
450
|
+
regis_calls: list[str] = dynamic(
|
451
|
+
"regis_call", f=registries
|
452
|
+
) # pragma: no cov
|
453
|
+
regis_calls.extend(["ddeutil.vendors"])
|
454
|
+
|
455
|
+
for module in regis_calls:
|
456
|
+
# NOTE: try to sequential import task functions
|
457
|
+
try:
|
458
|
+
importer = import_module(f"{module}.{submodule}")
|
459
|
+
except ModuleNotFoundError:
|
460
|
+
continue
|
461
|
+
|
462
|
+
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
463
|
+
# NOTE: check function attribute that already set tag by
|
464
|
+
# ``utils.tag`` decorator.
|
465
|
+
if not (
|
466
|
+
hasattr(func, "tag") and hasattr(func, "name")
|
467
|
+
): # pragma: no cov
|
468
|
+
continue
|
469
|
+
|
470
|
+
# NOTE: Define type of the func value.
|
471
|
+
func: TagFunc
|
472
|
+
|
473
|
+
# NOTE: Create new register name if it not exists
|
474
|
+
if func.name not in rs:
|
475
|
+
rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
|
476
|
+
continue
|
477
|
+
|
478
|
+
if func.tag in rs[func.name]:
|
479
|
+
raise ValueError(
|
480
|
+
f"The tag {func.tag!r} already exists on "
|
481
|
+
f"{module}.{submodule}, you should change this tag name or "
|
482
|
+
f"change it func name."
|
483
|
+
)
|
484
|
+
rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
|
485
|
+
|
486
|
+
return rs
|
487
|
+
|
488
|
+
|
489
|
+
@dataclass(frozen=True)
|
490
|
+
class CallSearchData:
|
491
|
+
"""Call Search dataclass that use for receive regular expression grouping
|
492
|
+
dict from searching call string value.
|
493
|
+
"""
|
494
|
+
|
495
|
+
path: str
|
496
|
+
func: str
|
497
|
+
tag: str
|
498
|
+
|
499
|
+
|
500
|
+
def extract_call(
|
501
|
+
call: str,
|
502
|
+
registries: Optional[list[str]] = None,
|
503
|
+
) -> Callable[[], TagFunc]:
|
504
|
+
"""Extract Call function from string value to call partial function that
|
505
|
+
does run it at runtime.
|
506
|
+
|
507
|
+
:param call: (str) A call value that able to match with Task regex.
|
508
|
+
:param registries: (Optional[list[str]]) A list of registry.
|
509
|
+
|
510
|
+
The format of call value should contain 3 regular expression groups
|
511
|
+
which match with the below config format:
|
512
|
+
|
513
|
+
>>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
|
514
|
+
|
515
|
+
Examples:
|
516
|
+
>>> extract_call("tasks/el-postgres-to-delta@polars")
|
517
|
+
...
|
518
|
+
>>> extract_call("tasks/return-type-not-valid@raise")
|
519
|
+
...
|
520
|
+
|
521
|
+
:raise NotImplementedError: When the searching call's function result does
|
522
|
+
not exist in the registry.
|
523
|
+
:raise NotImplementedError: When the searching call's tag result does not
|
524
|
+
exist in the registry with its function key.
|
525
|
+
|
526
|
+
:rtype: Callable[[], TagFunc]
|
527
|
+
"""
|
528
|
+
if not (found := Re.RE_TASK_FMT.search(call)):
|
529
|
+
raise ValueError(
|
530
|
+
f"Call {call!r} does not match with the call regex format."
|
531
|
+
)
|
532
|
+
|
533
|
+
call: CallSearchData = CallSearchData(**found.groupdict())
|
534
|
+
rgt: dict[str, Registry] = make_registry(
|
535
|
+
submodule=f"{call.path}",
|
536
|
+
registries=registries,
|
537
|
+
)
|
538
|
+
|
539
|
+
if call.func not in rgt:
|
540
|
+
raise NotImplementedError(
|
541
|
+
f"`REGISTER-MODULES.{call.path}.registries` not implement "
|
542
|
+
f"registry: {call.func!r}."
|
543
|
+
)
|
544
|
+
|
545
|
+
if call.tag not in rgt[call.func]:
|
546
|
+
raise NotImplementedError(
|
547
|
+
f"tag: {call.tag!r} not found on registry func: "
|
548
|
+
f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
|
549
|
+
)
|
550
|
+
return rgt[call.func][call.tag]
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
|
7
|
-
The main schedule running is `schedule_runner` function that trigger the
|
6
|
+
# [x] Use fix config
|
7
|
+
"""The main schedule running is `schedule_runner` function that trigger the
|
8
8
|
multiprocess of `schedule_control` function for listing schedules on the
|
9
9
|
config by `Loader.finds(Schedule)`.
|
10
10
|
|
@@ -52,10 +52,10 @@ except ImportError: # pragma: no cov
|
|
52
52
|
|
53
53
|
from .__cron import CronRunner
|
54
54
|
from .__types import DictData, TupleStr
|
55
|
-
from .audit import Audit, get_audit
|
56
55
|
from .conf import Loader, SimLoad, config, get_logger
|
57
56
|
from .cron import On
|
58
57
|
from .exceptions import ScheduleException, WorkflowException
|
58
|
+
from .logs import Audit, get_audit
|
59
59
|
from .result import Result, Status
|
60
60
|
from .utils import batch, delay
|
61
61
|
from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
|
@@ -86,9 +86,9 @@ class ScheduleWorkflow(BaseModel):
|
|
86
86
|
model.
|
87
87
|
|
88
88
|
This on field does not equal to the on field of Workflow model, but it
|
89
|
-
uses same logic to generate running release date with crontab object. It
|
90
|
-
for override the on field if the schedule time was change but you do
|
91
|
-
want to change on the workflow model.
|
89
|
+
uses same logic to generate running release date with crontab object. It
|
90
|
+
uses for override the on field if the schedule time was change, but you do
|
91
|
+
not want to change on the workflow model.
|
92
92
|
"""
|
93
93
|
|
94
94
|
alias: Optional[str] = Field(
|
@@ -177,7 +177,7 @@ class ScheduleWorkflow(BaseModel):
|
|
177
177
|
start_date: datetime,
|
178
178
|
queue: dict[str, ReleaseQueue],
|
179
179
|
*,
|
180
|
-
|
180
|
+
extras: DictData | None = None,
|
181
181
|
) -> list[WorkflowTask]:
|
182
182
|
"""Return the list of WorkflowTask object from the specific input
|
183
183
|
datetime that mapping with the on field.
|
@@ -187,17 +187,17 @@ class ScheduleWorkflow(BaseModel):
|
|
187
187
|
|
188
188
|
:param start_date: A start date that get from the workflow schedule.
|
189
189
|
:param queue: A mapping of name and list of datetime for queue.
|
190
|
-
:param
|
190
|
+
:param extras: An extra parameters that pass to the Loader object.
|
191
191
|
|
192
192
|
:rtype: list[WorkflowTask]
|
193
193
|
:return: Return the list of WorkflowTask object from the specific
|
194
194
|
input datetime that mapping with the on field.
|
195
195
|
"""
|
196
196
|
workflow_tasks: list[WorkflowTask] = []
|
197
|
-
extras: DictData =
|
197
|
+
extras: DictData = extras or {}
|
198
198
|
|
199
199
|
# NOTE: Loading workflow model from the name of workflow.
|
200
|
-
wf: Workflow = Workflow.
|
200
|
+
wf: Workflow = Workflow.from_conf(self.name, extras=extras)
|
201
201
|
wf_queue: ReleaseQueue = queue[self.alias]
|
202
202
|
|
203
203
|
# IMPORTANT: Create the default 'on' value if it does not pass the `on`
|
@@ -254,24 +254,24 @@ class Schedule(BaseModel):
|
|
254
254
|
return dedent(value)
|
255
255
|
|
256
256
|
@classmethod
|
257
|
-
def
|
257
|
+
def from_conf(
|
258
258
|
cls,
|
259
259
|
name: str,
|
260
|
-
|
260
|
+
extras: DictData | None = None,
|
261
261
|
) -> Self:
|
262
262
|
"""Create Schedule instance from the Loader object that only receive
|
263
263
|
an input schedule name. The loader object will use this schedule name to
|
264
264
|
searching configuration data of this schedule model in conf path.
|
265
265
|
|
266
266
|
:param name: (str) A schedule name that want to pass to Loader object.
|
267
|
-
:param
|
267
|
+
:param extras: An extra parameters that want to pass to Loader
|
268
268
|
object.
|
269
269
|
|
270
270
|
:raise ValueError: If the type does not match with current object.
|
271
271
|
|
272
272
|
:rtype: Self
|
273
273
|
"""
|
274
|
-
loader: Loader = Loader(name, externals=(
|
274
|
+
loader: Loader = Loader(name, externals=(extras or {}))
|
275
275
|
|
276
276
|
# NOTE: Validate the config type match with current connection model
|
277
277
|
if loader.type != cls.__name__:
|
@@ -325,16 +325,16 @@ class Schedule(BaseModel):
|
|
325
325
|
start_date: datetime,
|
326
326
|
queue: dict[str, ReleaseQueue],
|
327
327
|
*,
|
328
|
-
|
328
|
+
extras: DictData | None = None,
|
329
329
|
) -> list[WorkflowTask]:
|
330
330
|
"""Return the list of WorkflowTask object from the specific input
|
331
331
|
datetime that mapping with the on field from workflow schedule model.
|
332
332
|
|
333
333
|
:param start_date: A start date that get from the workflow schedule.
|
334
|
-
:param queue: A mapping of name and list of
|
335
|
-
|
336
|
-
:param
|
337
|
-
|
334
|
+
:param queue: (dict[str, ReleaseQueue]) A mapping of name and list of
|
335
|
+
datetime for queue.
|
336
|
+
:param extras: (DictData) An extra parameters that pass to the Loader
|
337
|
+
object.
|
338
338
|
|
339
339
|
:rtype: list[WorkflowTask]
|
340
340
|
:return: Return the list of WorkflowTask object from the specific
|
@@ -348,7 +348,7 @@ class Schedule(BaseModel):
|
|
348
348
|
queue[workflow.alias] = ReleaseQueue()
|
349
349
|
|
350
350
|
workflow_tasks.extend(
|
351
|
-
workflow.tasks(start_date, queue=queue,
|
351
|
+
workflow.tasks(start_date, queue=queue, extras=extras)
|
352
352
|
)
|
353
353
|
|
354
354
|
return workflow_tasks
|
@@ -357,14 +357,14 @@ class Schedule(BaseModel):
|
|
357
357
|
self,
|
358
358
|
*,
|
359
359
|
stop: datetime | None = None,
|
360
|
-
|
360
|
+
extras: DictData | None = None,
|
361
361
|
audit: type[Audit] | None = None,
|
362
362
|
parent_run_id: str | None = None,
|
363
363
|
) -> Result: # pragma: no cov
|
364
364
|
"""Pending this schedule tasks with the schedule package.
|
365
365
|
|
366
366
|
:param stop: A datetime value that use to stop running schedule.
|
367
|
-
:param
|
367
|
+
:param extras: An extra parameters that pass to Loader.
|
368
368
|
:param audit: An audit class that use on the workflow task release for
|
369
369
|
writing its release audit context.
|
370
370
|
:param parent_run_id: A parent workflow running ID for this release.
|
@@ -385,9 +385,7 @@ class Schedule(BaseModel):
|
|
385
385
|
) + timedelta(minutes=1)
|
386
386
|
|
387
387
|
scheduler_pending(
|
388
|
-
tasks=self.tasks(
|
389
|
-
start_date_waiting, queue=queue, externals=externals
|
390
|
-
),
|
388
|
+
tasks=self.tasks(start_date_waiting, queue=queue, extras=extras),
|
391
389
|
stop=stop_date,
|
392
390
|
queue=queue,
|
393
391
|
threads=threads,
|
@@ -709,7 +707,7 @@ def scheduler_pending(
|
|
709
707
|
def schedule_control(
|
710
708
|
schedules: list[str],
|
711
709
|
stop: datetime | None = None,
|
712
|
-
|
710
|
+
extras: DictData | None = None,
|
713
711
|
*,
|
714
712
|
audit: type[Audit] | None = None,
|
715
713
|
parent_run_id: str | None = None,
|
@@ -720,7 +718,7 @@ def schedule_control(
|
|
720
718
|
|
721
719
|
:param schedules: A list of workflow names that want to schedule running.
|
722
720
|
:param stop: A datetime value that use to stop running schedule.
|
723
|
-
:param
|
721
|
+
:param extras: An extra parameters that pass to Loader.
|
724
722
|
:param audit: An audit class that use on the workflow task release for
|
725
723
|
writing its release audit context.
|
726
724
|
:param parent_run_id: A parent workflow running ID for this release.
|
@@ -745,10 +743,10 @@ def schedule_control(
|
|
745
743
|
tasks: list[WorkflowTask] = []
|
746
744
|
for name in schedules:
|
747
745
|
tasks.extend(
|
748
|
-
Schedule.
|
746
|
+
Schedule.from_conf(name, extras=extras).tasks(
|
749
747
|
start_date_waiting,
|
750
748
|
queue=queue,
|
751
|
-
|
749
|
+
extras=extras,
|
752
750
|
),
|
753
751
|
)
|
754
752
|
|