ddeutil-workflow 0.0.15__py3-none-any.whl → 0.0.17__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/{cron.py → __cron.py} +12 -6
- ddeutil/workflow/__init__.py +1 -0
- ddeutil/workflow/__types.py +18 -6
- ddeutil/workflow/api.py +3 -5
- ddeutil/workflow/cli.py +2 -6
- ddeutil/workflow/conf.py +441 -3
- ddeutil/workflow/job.py +119 -62
- ddeutil/workflow/on.py +11 -8
- ddeutil/workflow/repeat.py +2 -6
- ddeutil/workflow/route.py +4 -12
- ddeutil/workflow/scheduler.py +71 -54
- ddeutil/workflow/stage.py +79 -43
- ddeutil/workflow/utils.py +96 -283
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/METADATA +44 -25
- ddeutil_workflow-0.0.17.dist-info/RECORD +21 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/WHEEL +1 -1
- ddeutil/workflow/log.py +0 -198
- ddeutil_workflow-0.0.15.dist-info/RECORD +0 -22
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""Job Model that use for keeping stages and node that running its stages.
|
7
|
+
The job handle the lineage of stages and location of execution of stages that
|
8
|
+
mean the job model able to define ``runs-on`` key that allow you to run this
|
9
|
+
job.
|
7
10
|
"""
|
8
11
|
from __future__ import annotations
|
9
12
|
|
@@ -19,27 +22,20 @@ from concurrent.futures import (
|
|
19
22
|
from functools import lru_cache
|
20
23
|
from textwrap import dedent
|
21
24
|
from threading import Event
|
22
|
-
from typing import Optional
|
25
|
+
from typing import Optional, Union
|
23
26
|
|
24
27
|
from ddeutil.core import freeze_args
|
25
28
|
from pydantic import BaseModel, Field
|
26
29
|
from pydantic.functional_validators import field_validator, model_validator
|
27
30
|
from typing_extensions import Self
|
28
31
|
|
29
|
-
from .__types import
|
30
|
-
|
31
|
-
DictStr,
|
32
|
-
Matrix,
|
33
|
-
MatrixExclude,
|
34
|
-
MatrixInclude,
|
35
|
-
TupleStr,
|
36
|
-
)
|
32
|
+
from .__types import DictData, DictStr, Matrix, TupleStr
|
33
|
+
from .conf import config, get_logger
|
37
34
|
from .exceptions import (
|
38
35
|
JobException,
|
39
36
|
StageException,
|
40
37
|
UtilException,
|
41
38
|
)
|
42
|
-
from .log import get_logger
|
43
39
|
from .stage import Stage
|
44
40
|
from .utils import (
|
45
41
|
Result,
|
@@ -51,6 +47,8 @@ from .utils import (
|
|
51
47
|
)
|
52
48
|
|
53
49
|
logger = get_logger("ddeutil.workflow")
|
50
|
+
MatrixInclude = list[dict[str, Union[str, int]]]
|
51
|
+
MatrixExclude = list[dict[str, Union[str, int]]]
|
54
52
|
|
55
53
|
|
56
54
|
__all__: TupleStr = (
|
@@ -112,6 +110,7 @@ def make(
|
|
112
110
|
all(inc.get(k) == v for k, v in m.items()) for m in [*final, *add]
|
113
111
|
):
|
114
112
|
continue
|
113
|
+
|
115
114
|
add.append(inc)
|
116
115
|
|
117
116
|
# NOTE: Merge all matrix together.
|
@@ -262,7 +261,7 @@ class Job(BaseModel):
|
|
262
261
|
)
|
263
262
|
|
264
263
|
@model_validator(mode="before")
|
265
|
-
def
|
264
|
+
def __prepare_keys__(cls, values: DictData) -> DictData:
|
266
265
|
"""Rename key that use dash to underscore because Python does not
|
267
266
|
support this character exist in any variable name.
|
268
267
|
|
@@ -273,12 +272,33 @@ class Job(BaseModel):
|
|
273
272
|
return values
|
274
273
|
|
275
274
|
@field_validator("desc", mode="after")
|
276
|
-
def
|
277
|
-
"""Prepare description string that was created on a template.
|
275
|
+
def ___prepare_desc__(cls, value: str) -> str:
|
276
|
+
"""Prepare description string that was created on a template.
|
277
|
+
|
278
|
+
:rtype: str
|
279
|
+
"""
|
278
280
|
return dedent(value)
|
279
281
|
|
282
|
+
@field_validator("stages", mode="after")
|
283
|
+
def __validate_stage_id__(cls, value: list[Stage]) -> list[Stage]:
|
284
|
+
"""Validate a stage ID of all stage in stages field should not be
|
285
|
+
duplicate.
|
286
|
+
|
287
|
+
:rtype: list[Stage]
|
288
|
+
"""
|
289
|
+
# VALIDATE: Validate stage id should not duplicate.
|
290
|
+
rs: list[str] = []
|
291
|
+
for stage in value:
|
292
|
+
name: str = stage.id or stage.name
|
293
|
+
if name in rs:
|
294
|
+
raise ValueError(
|
295
|
+
"Stage name in jobs object should not be duplicate."
|
296
|
+
)
|
297
|
+
rs.append(name)
|
298
|
+
return value
|
299
|
+
|
280
300
|
@model_validator(mode="after")
|
281
|
-
def
|
301
|
+
def __prepare_running_id_and_stage_name__(self) -> Self:
|
282
302
|
"""Prepare the job running ID.
|
283
303
|
|
284
304
|
:rtype: Self
|
@@ -319,34 +339,44 @@ class Job(BaseModel):
|
|
319
339
|
For example of setting output method, If you receive execute output
|
320
340
|
and want to set on the `to` like;
|
321
341
|
|
322
|
-
... (i) output: {'
|
323
|
-
... (ii) to: {'jobs'}
|
342
|
+
... (i) output: {'strategy-01': bar, 'strategy-02': bar}
|
343
|
+
... (ii) to: {'jobs': {}}
|
324
344
|
|
325
345
|
The result of the `to` variable will be;
|
326
346
|
|
327
347
|
... (iii) to: {
|
328
|
-
|
348
|
+
'jobs': {
|
349
|
+
'<job-id>': {
|
329
350
|
'strategies': {
|
330
|
-
'
|
351
|
+
'strategy-01': bar,
|
352
|
+
'strategy-02': bar
|
331
353
|
}
|
332
354
|
}
|
333
355
|
}
|
356
|
+
}
|
334
357
|
|
335
358
|
:param output: An output context.
|
336
359
|
:param to: A context data that want to add output result.
|
337
360
|
:rtype: DictData
|
338
361
|
"""
|
339
|
-
if self.id is None:
|
362
|
+
if self.id is None and not config.job_default_id:
|
340
363
|
raise JobException(
|
341
|
-
"This job do not set the ID before setting output."
|
364
|
+
"This job do not set the ID before setting execution output."
|
342
365
|
)
|
343
366
|
|
344
|
-
|
367
|
+
# NOTE: Create jobs key to receive an output from the job execution.
|
368
|
+
if "jobs" not in to:
|
369
|
+
to["jobs"] = {}
|
370
|
+
|
371
|
+
# NOTE: If the job ID did not set, it will use index of jobs key
|
372
|
+
# instead.
|
373
|
+
_id: str = self.id or str(len(to["jobs"]) + 1)
|
374
|
+
|
375
|
+
logger.debug(f"({self.run_id}) [JOB]: Set outputs on: {_id}")
|
376
|
+
to["jobs"][_id] = (
|
345
377
|
{"strategies": output}
|
346
378
|
if self.strategy.is_set()
|
347
|
-
|
348
|
-
# This is the best way to get single key from dict.
|
349
|
-
else output[next(iter(output))]
|
379
|
+
else output.get(next(iter(output), "DUMMY"), {})
|
350
380
|
)
|
351
381
|
return to
|
352
382
|
|
@@ -356,7 +386,6 @@ class Job(BaseModel):
|
|
356
386
|
params: DictData,
|
357
387
|
*,
|
358
388
|
event: Event | None = None,
|
359
|
-
raise_error: bool = True,
|
360
389
|
) -> Result:
|
361
390
|
"""Job Strategy execution with passing dynamic parameters from the
|
362
391
|
workflow execution to strategy matrix.
|
@@ -365,25 +394,27 @@ class Job(BaseModel):
|
|
365
394
|
It different with ``self.execute`` because this method run only one
|
366
395
|
strategy and return with context of this strategy data.
|
367
396
|
|
368
|
-
:raise JobException: If it has any error from StageException or
|
369
|
-
UtilException
|
397
|
+
:raise JobException: If it has any error from ``StageException`` or
|
398
|
+
``UtilException``.
|
370
399
|
|
371
400
|
:param strategy: A metrix strategy value.
|
372
401
|
:param params: A dynamic parameters.
|
373
402
|
:param event: An manger event that pass to the PoolThreadExecutor.
|
374
|
-
|
375
|
-
if it get exception from stage execution.
|
403
|
+
|
376
404
|
:rtype: Result
|
377
405
|
"""
|
378
406
|
strategy_id: str = gen_id(strategy)
|
379
407
|
|
380
|
-
#
|
408
|
+
# PARAGRAPH:
|
409
|
+
#
|
410
|
+
# Create strategy execution context and update a matrix and copied
|
381
411
|
# of params. So, the context value will have structure like;
|
382
412
|
#
|
383
413
|
# {
|
384
414
|
# "params": { ... }, <== Current input params
|
385
415
|
# "jobs": { ... }, <== Current input params
|
386
416
|
# "matrix": { ... } <== Current strategy value
|
417
|
+
# "stages": { ... } <== Catching stage outputs
|
387
418
|
# }
|
388
419
|
#
|
389
420
|
context: DictData = copy.deepcopy(params)
|
@@ -395,14 +426,14 @@ class Job(BaseModel):
|
|
395
426
|
# IMPORTANT: Change any stage running IDs to this job running ID.
|
396
427
|
stage: Stage = stage.get_running_id(self.run_id)
|
397
428
|
|
398
|
-
|
429
|
+
name: str = stage.id or stage.name
|
399
430
|
|
400
431
|
if stage.is_skipped(params=context):
|
401
|
-
logger.info(f"({self.run_id}) [JOB]: Skip stage: {
|
432
|
+
logger.info(f"({self.run_id}) [JOB]: Skip stage: {name!r}")
|
402
433
|
continue
|
403
434
|
|
404
435
|
logger.info(
|
405
|
-
f"({self.run_id}) [JOB]: Start execute the stage: {
|
436
|
+
f"({self.run_id}) [JOB]: Start execute the stage: {name!r}"
|
406
437
|
)
|
407
438
|
|
408
439
|
# NOTE: Logging a matrix that pass on this stage execution.
|
@@ -422,20 +453,20 @@ class Job(BaseModel):
|
|
422
453
|
# ---
|
423
454
|
# "stages": filter_func(context.pop("stages", {})),
|
424
455
|
"stages": context.pop("stages", {}),
|
425
|
-
# NOTE: Set the error keys.
|
426
456
|
"error": JobException(
|
427
|
-
"
|
457
|
+
"Job strategy was canceled from trigger event "
|
458
|
+
"that had stopped before execution."
|
459
|
+
),
|
460
|
+
"error_message": (
|
461
|
+
"Job strategy was canceled from trigger event "
|
462
|
+
"that had stopped before execution."
|
428
463
|
),
|
429
|
-
"error_message": {
|
430
|
-
"message": (
|
431
|
-
"Process Event stopped before execution"
|
432
|
-
),
|
433
|
-
},
|
434
464
|
},
|
435
465
|
},
|
436
466
|
)
|
437
467
|
|
438
|
-
#
|
468
|
+
# PARAGRAPH:
|
469
|
+
#
|
439
470
|
# I do not use below syntax because `params` dict be the
|
440
471
|
# reference memory pointer and it was changed when I action
|
441
472
|
# anything like update or re-construct this.
|
@@ -461,16 +492,25 @@ class Job(BaseModel):
|
|
461
492
|
logger.error(
|
462
493
|
f"({self.run_id}) [JOB]: {err.__class__.__name__}: {err}"
|
463
494
|
)
|
464
|
-
if
|
495
|
+
if config.job_raise_error:
|
465
496
|
raise JobException(
|
466
497
|
f"Get stage execution error: {err.__class__.__name__}: "
|
467
498
|
f"{err}"
|
468
499
|
) from None
|
469
|
-
|
470
|
-
|
500
|
+
return Result(
|
501
|
+
status=1,
|
502
|
+
context={
|
503
|
+
strategy_id: {
|
504
|
+
"matrix": strategy,
|
505
|
+
"stages": context.pop("stages", {}),
|
506
|
+
"error": err,
|
507
|
+
"error_message": f"{err.__class__.__name__}: {err}",
|
508
|
+
},
|
509
|
+
},
|
510
|
+
)
|
471
511
|
|
472
|
-
# NOTE: Remove
|
473
|
-
# ``get_running_id`` method.
|
512
|
+
# NOTE: Remove the current stage object that was created from
|
513
|
+
# ``get_running_id`` method for saving memory.
|
474
514
|
del stage
|
475
515
|
|
476
516
|
return Result(
|
@@ -491,15 +531,18 @@ class Job(BaseModel):
|
|
491
531
|
:param params: An input parameters that use on job execution.
|
492
532
|
:rtype: Result
|
493
533
|
"""
|
494
|
-
|
534
|
+
|
535
|
+
# NOTE: I use this condition because this method allow passing empty
|
536
|
+
# params and I do not want to create new dict object.
|
495
537
|
params: DictData = {} if params is None else params
|
538
|
+
context: DictData = {}
|
496
539
|
|
497
540
|
# NOTE: Normal Job execution without parallel strategy.
|
498
541
|
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
499
542
|
for strategy in self.strategy.make():
|
500
543
|
rs: Result = self.execute_strategy(
|
501
544
|
strategy=strategy,
|
502
|
-
params=
|
545
|
+
params=params,
|
503
546
|
)
|
504
547
|
context.update(rs.context)
|
505
548
|
return Result(
|
@@ -507,11 +550,17 @@ class Job(BaseModel):
|
|
507
550
|
context=context,
|
508
551
|
)
|
509
552
|
|
510
|
-
# NOTE: Create event for cancel executor stop running.
|
553
|
+
# NOTE: Create event for cancel executor by trigger stop running event.
|
511
554
|
event: Event = Event()
|
512
555
|
|
556
|
+
print("Job Run Fail-Fast:", self.strategy.fail_fast)
|
557
|
+
|
558
|
+
# IMPORTANT: Start running strategy execution by multithreading because
|
559
|
+
# it will running by strategy values without waiting previous
|
560
|
+
# execution.
|
513
561
|
with ThreadPoolExecutor(
|
514
|
-
max_workers=self.strategy.max_parallel
|
562
|
+
max_workers=self.strategy.max_parallel,
|
563
|
+
thread_name_prefix="job_strategy_exec_",
|
515
564
|
) as executor:
|
516
565
|
futures: list[Future] = [
|
517
566
|
executor.submit(
|
@@ -566,30 +615,34 @@ class Job(BaseModel):
|
|
566
615
|
)
|
567
616
|
logger.debug(f"({self.run_id}) [JOB]: Strategy is set Fail Fast{nd}")
|
568
617
|
|
569
|
-
# NOTE:
|
618
|
+
# NOTE:
|
619
|
+
# Stop all running tasks with setting the event manager and cancel
|
570
620
|
# any scheduled tasks.
|
621
|
+
#
|
571
622
|
if len(done) != len(futures):
|
572
623
|
event.set()
|
573
|
-
for future in
|
624
|
+
for future in not_done:
|
574
625
|
future.cancel()
|
575
626
|
|
576
|
-
|
577
|
-
|
627
|
+
future: Future
|
578
628
|
for future in done:
|
579
|
-
if future.exception():
|
580
|
-
status = 1
|
629
|
+
if err := future.exception():
|
630
|
+
status: int = 1
|
581
631
|
logger.error(
|
582
632
|
f"({self.run_id}) [JOB]: One stage failed with: "
|
583
633
|
f"{future.exception()}, shutting down this future."
|
584
634
|
)
|
585
|
-
|
635
|
+
context.update(
|
636
|
+
{
|
637
|
+
"error": err,
|
638
|
+
"error_message": f"{err.__class__.__name__}: {err}",
|
639
|
+
},
|
640
|
+
)
|
586
641
|
continue
|
587
642
|
|
588
643
|
# NOTE: Update the result context to main job context.
|
589
644
|
context.update(future.result(timeout=result_timeout).context)
|
590
645
|
|
591
|
-
del future
|
592
|
-
|
593
646
|
return rs_final.catch(status=status, context=context)
|
594
647
|
|
595
648
|
def __catch_all_completed(
|
@@ -614,7 +667,7 @@ class Job(BaseModel):
|
|
614
667
|
for future in as_completed(futures, timeout=timeout):
|
615
668
|
try:
|
616
669
|
context.update(future.result(timeout=result_timeout).context)
|
617
|
-
except TimeoutError:
|
670
|
+
except TimeoutError: # pragma: no cov
|
618
671
|
status = 1
|
619
672
|
logger.warning(
|
620
673
|
f"({self.run_id}) [JOB]: Task is hanging. Attempting to "
|
@@ -636,6 +689,10 @@ class Job(BaseModel):
|
|
636
689
|
f"fail-fast does not set;\n{err.__class__.__name__}:\n\t"
|
637
690
|
f"{err}"
|
638
691
|
)
|
639
|
-
|
640
|
-
|
692
|
+
context.update(
|
693
|
+
{
|
694
|
+
"error": err,
|
695
|
+
"error_message": f"{err.__class__.__name__}: {err}",
|
696
|
+
},
|
697
|
+
)
|
641
698
|
return rs_final.catch(status=status, context=context)
|
ddeutil/workflow/on.py
CHANGED
@@ -14,9 +14,9 @@ from pydantic.functional_serializers import field_serializer
|
|
14
14
|
from pydantic.functional_validators import field_validator, model_validator
|
15
15
|
from typing_extensions import Self
|
16
16
|
|
17
|
+
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner
|
17
18
|
from .__types import DictData, DictStr, TupleStr
|
18
|
-
from .
|
19
|
-
from .utils import Loader
|
19
|
+
from .conf import Loader
|
20
20
|
|
21
21
|
__all__: TupleStr = (
|
22
22
|
"On",
|
@@ -109,7 +109,7 @@ class On(BaseModel):
|
|
109
109
|
def from_loader(
|
110
110
|
cls,
|
111
111
|
name: str,
|
112
|
-
externals: DictData,
|
112
|
+
externals: DictData | None = None,
|
113
113
|
) -> Self:
|
114
114
|
"""Constructor from the name of config that will use loader object for
|
115
115
|
getting the data.
|
@@ -117,6 +117,7 @@ class On(BaseModel):
|
|
117
117
|
:param name: A name of config that will getting from loader.
|
118
118
|
:param externals: A extras external parameter that will keep in extras.
|
119
119
|
"""
|
120
|
+
externals: DictData = externals or {}
|
120
121
|
loader: Loader = Loader(name, externals=externals)
|
121
122
|
|
122
123
|
# NOTE: Validate the config type match with current connection model
|
@@ -139,7 +140,9 @@ class On(BaseModel):
|
|
139
140
|
)
|
140
141
|
)
|
141
142
|
if "cronjob" not in loader_data:
|
142
|
-
raise ValueError(
|
143
|
+
raise ValueError(
|
144
|
+
"Config does not set ``cronjob`` or ``interval`` keys"
|
145
|
+
)
|
143
146
|
return cls.model_validate(
|
144
147
|
obj=dict(
|
145
148
|
cronjob=loader_data.pop("cronjob"),
|
@@ -175,17 +178,17 @@ class On(BaseModel):
|
|
175
178
|
|
176
179
|
def generate(self, start: str | datetime) -> CronRunner:
|
177
180
|
"""Return Cron runner object."""
|
178
|
-
if
|
181
|
+
if isinstance(start, str):
|
179
182
|
start: datetime = datetime.fromisoformat(start)
|
183
|
+
elif not isinstance(start, datetime):
|
184
|
+
raise TypeError("start value should be str or datetime type.")
|
180
185
|
return self.cronjob.schedule(date=start, tz=self.tz)
|
181
186
|
|
182
187
|
def next(self, start: str | datetime) -> datetime:
|
183
188
|
"""Return a next datetime from Cron runner object that start with any
|
184
189
|
date that given from input.
|
185
190
|
"""
|
186
|
-
|
187
|
-
start: datetime = datetime.fromisoformat(start)
|
188
|
-
return self.cronjob.schedule(date=start, tz=self.tz).next
|
191
|
+
return self.generate(start=start).next
|
189
192
|
|
190
193
|
|
191
194
|
class YearOn(On):
|
ddeutil/workflow/repeat.py
CHANGED
@@ -6,16 +6,14 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import asyncio
|
9
|
-
import os
|
10
9
|
from asyncio import ensure_future
|
11
10
|
from datetime import datetime
|
12
11
|
from functools import wraps
|
13
|
-
from zoneinfo import ZoneInfo
|
14
12
|
|
15
13
|
from starlette.concurrency import run_in_threadpool
|
16
14
|
|
15
|
+
from .conf import config, get_logger
|
17
16
|
from .cron import CronJob
|
18
|
-
from .log import get_logger
|
19
17
|
|
20
18
|
logger = get_logger("ddeutil.workflow")
|
21
19
|
|
@@ -24,9 +22,7 @@ def get_cronjob_delta(cron: str) -> float:
|
|
24
22
|
"""This function returns the time delta between now and the next cron
|
25
23
|
execution time.
|
26
24
|
"""
|
27
|
-
now: datetime = datetime.now(
|
28
|
-
tz=ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
|
29
|
-
)
|
25
|
+
now: datetime = datetime.now(tz=config.tz)
|
30
26
|
cron = CronJob(cron)
|
31
27
|
return (cron.schedule(now).next - now).total_seconds()
|
32
28
|
|
ddeutil/workflow/route.py
CHANGED
@@ -6,10 +6,8 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import copy
|
9
|
-
import os
|
10
9
|
from datetime import datetime, timedelta
|
11
10
|
from typing import Any
|
12
|
-
from zoneinfo import ZoneInfo
|
13
11
|
|
14
12
|
from fastapi import APIRouter, HTTPException, Request
|
15
13
|
from fastapi import status as st
|
@@ -18,9 +16,9 @@ from pydantic import BaseModel
|
|
18
16
|
|
19
17
|
from . import Workflow
|
20
18
|
from .__types import DictData
|
21
|
-
from .
|
19
|
+
from .conf import Loader, config, get_logger
|
22
20
|
from .scheduler import Schedule
|
23
|
-
from .utils import
|
21
|
+
from .utils import Result
|
24
22
|
|
25
23
|
logger = get_logger("ddeutil.workflow")
|
26
24
|
workflow = APIRouter(
|
@@ -87,12 +85,7 @@ async def execute_workflow(name: str, payload: ExecutePayload) -> DictData:
|
|
87
85
|
# NOTE: Start execute manually
|
88
86
|
rs: Result = wf.execute(params=payload.params)
|
89
87
|
|
90
|
-
return rs
|
91
|
-
by_alias=True,
|
92
|
-
exclude_none=True,
|
93
|
-
exclude_unset=True,
|
94
|
-
exclude_defaults=True,
|
95
|
-
)
|
88
|
+
return dict(rs)
|
96
89
|
|
97
90
|
|
98
91
|
@workflow.get("/{name}/logs")
|
@@ -172,8 +165,7 @@ async def add_deploy_scheduler(request: Request, name: str):
|
|
172
165
|
|
173
166
|
request.state.scheduler.append(name)
|
174
167
|
|
175
|
-
|
176
|
-
start_date: datetime = datetime.now(tz=tz)
|
168
|
+
start_date: datetime = datetime.now(tz=config.tz)
|
177
169
|
start_date_waiting: datetime = (start_date + timedelta(minutes=1)).replace(
|
178
170
|
second=0, microsecond=0
|
179
171
|
)
|