ddeutil-workflow 0.0.23__py3-none-any.whl → 0.0.25__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.
@@ -4,7 +4,7 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  """
7
- The main schedule running is ``workflow_runner`` function that trigger the
7
+ The main schedule running is ``schedule_runner`` function that trigger the
8
8
  multiprocess of ``workflow_control`` function for listing schedules on the
9
9
  config by ``Loader.finds(Schedule)``.
10
10
 
@@ -21,7 +21,6 @@ value with the on field.
21
21
  from __future__ import annotations
22
22
 
23
23
  import copy
24
- import inspect
25
24
  import logging
26
25
  import time
27
26
  from concurrent.futures import (
@@ -31,9 +30,10 @@ from concurrent.futures import (
31
30
  )
32
31
  from datetime import datetime, timedelta
33
32
  from functools import wraps
33
+ from heapq import heappop, heappush
34
34
  from textwrap import dedent
35
35
  from threading import Thread
36
- from typing import Callable, Optional
36
+ from typing import Callable, Optional, TypedDict
37
37
 
38
38
  from pydantic import BaseModel, Field
39
39
  from pydantic.functional_validators import field_validator, model_validator
@@ -51,30 +51,31 @@ except ImportError: # pragma: no cov
51
51
 
52
52
  from .__cron import CronRunner
53
53
  from .__types import DictData, TupleStr
54
- from .conf import Loader, config, get_logger
54
+ from .conf import FileLog, Loader, Log, config, get_logger
55
+ from .cron import On
55
56
  from .exceptions import WorkflowException
56
- from .on import On
57
57
  from .utils import (
58
58
  batch,
59
59
  delay,
60
- queue2str,
61
60
  )
62
- from .workflow import Workflow, WorkflowTaskData
61
+ from .workflow import Workflow, WorkflowQueue, WorkflowRelease, WorkflowTask
63
62
 
64
63
  P = ParamSpec("P")
65
64
  logger = get_logger("ddeutil.workflow")
66
65
 
67
- # NOTE: Adjust logging level on the schedule package.
66
+ # NOTE: Adjust logging level on the `schedule` package.
68
67
  logging.getLogger("schedule").setLevel(logging.INFO)
69
68
 
70
69
 
71
70
  __all__: TupleStr = (
72
71
  "Schedule",
73
72
  "WorkflowSchedule",
74
- "workflow_task_release",
75
- "workflow_monitor",
76
- "workflow_control",
77
- "workflow_runner",
73
+ "schedule_task",
74
+ "monitor",
75
+ "schedule_control",
76
+ "schedule_runner",
77
+ "ReleaseThreads",
78
+ "ReleaseThread",
78
79
  )
79
80
 
80
81
 
@@ -166,6 +167,58 @@ class WorkflowSchedule(BaseModel):
166
167
 
167
168
  return value
168
169
 
170
+ def tasks(
171
+ self,
172
+ start_date: datetime,
173
+ queue: dict[str, WorkflowQueue],
174
+ *,
175
+ externals: DictData | None = None,
176
+ ) -> list[WorkflowTask]:
177
+ """Return the list of WorkflowTask object from the specific input
178
+ datetime that mapping with the on field.
179
+
180
+ This task creation need queue to tracking release date already
181
+ mapped or not.
182
+
183
+ :param start_date: A start date that get from the workflow schedule.
184
+ :param queue: A mapping of name and list of datetime for queue.
185
+ :param externals: An external parameters that pass to the Loader object.
186
+
187
+ :rtype: list[WorkflowTask]
188
+ :return: Return the list of WorkflowTask object from the specific
189
+ input datetime that mapping with the on field.
190
+ """
191
+ workflow_tasks: list[WorkflowTask] = []
192
+ extras: DictData = externals or {}
193
+
194
+ # NOTE: Loading workflow model from the name of workflow.
195
+ wf: Workflow = Workflow.from_loader(self.name, externals=extras)
196
+ wf_queue: WorkflowQueue = queue[self.alias]
197
+
198
+ # IMPORTANT: Create the default 'on' value if it does not passing
199
+ # the on field to the Schedule object.
200
+ ons: list[On] = self.on or wf.on.copy()
201
+
202
+ for on in ons:
203
+
204
+ # NOTE: Create CronRunner instance from the start_date param.
205
+ runner: CronRunner = on.generate(start_date)
206
+ next_running_date = runner.next
207
+
208
+ while wf_queue.check_queue(next_running_date):
209
+ next_running_date = runner.next
210
+
211
+ workflow_tasks.append(
212
+ WorkflowTask(
213
+ alias=self.alias,
214
+ workflow=wf,
215
+ runner=runner,
216
+ values=self.values,
217
+ ),
218
+ )
219
+
220
+ return workflow_tasks
221
+
169
222
 
170
223
  class Schedule(BaseModel):
171
224
  """Schedule Pydantic model that use to run with any scheduler package.
@@ -214,7 +267,7 @@ class Schedule(BaseModel):
214
267
  loader: Loader = Loader(name, externals=(externals or {}))
215
268
 
216
269
  # NOTE: Validate the config type match with current connection model
217
- if loader.type != cls:
270
+ if loader.type != cls.__name__:
218
271
  raise ValueError(f"Type {loader.type} does not match with {cls}")
219
272
 
220
273
  loader_data: DictData = copy.deepcopy(loader.data)
@@ -227,57 +280,33 @@ class Schedule(BaseModel):
227
280
  def tasks(
228
281
  self,
229
282
  start_date: datetime,
230
- queue: dict[str, list[datetime]],
283
+ queue: dict[str, WorkflowQueue],
231
284
  *,
232
285
  externals: DictData | None = None,
233
- ) -> list[WorkflowTaskData]:
234
- """Return the list of WorkflowTaskData object from the specific input
235
- datetime that mapping with the on field.
236
-
237
- This task creation need queue to tracking release date already
238
- mapped or not.
286
+ ) -> list[WorkflowTask]:
287
+ """Return the list of WorkflowTask object from the specific input
288
+ datetime that mapping with the on field from workflow schedule model.
239
289
 
240
290
  :param start_date: A start date that get from the workflow schedule.
241
291
  :param queue: A mapping of name and list of datetime for queue.
292
+ :type queue: dict[str, WorkflowQueue]
242
293
  :param externals: An external parameters that pass to the Loader object.
294
+ :type externals: DictData | None
243
295
 
244
- :rtype: list[WorkflowTaskData]
245
- :return: Return the list of WorkflowTaskData object from the specific
296
+ :rtype: list[WorkflowTask]
297
+ :return: Return the list of WorkflowTask object from the specific
246
298
  input datetime that mapping with the on field.
247
299
  """
248
- workflow_tasks: list[WorkflowTaskData] = []
249
- extras: DictData = externals or {}
300
+ workflow_tasks: list[WorkflowTask] = []
250
301
 
251
- for sch_wf in self.workflows:
302
+ for workflow in self.workflows:
252
303
 
253
- # NOTE: Loading workflow model from the name of workflow.
254
- wf: Workflow = Workflow.from_loader(sch_wf.name, externals=extras)
304
+ if workflow.alias not in queue:
305
+ queue[workflow.alias] = WorkflowQueue()
255
306
 
256
- # NOTE: Create default list of release datetime by empty list.
257
- if sch_wf.alias not in queue:
258
- queue[sch_wf.alias]: list[datetime] = []
259
-
260
- # IMPORTANT: Create the default 'on' value if it does not passing
261
- # the on field to the Schedule object.
262
- ons: list[On] = sch_wf.on or wf.on.copy()
263
-
264
- for on in ons:
265
-
266
- # NOTE: Create CronRunner instance from the start_date param.
267
- runner: CronRunner = on.generate(start_date)
268
- next_running_date = runner.next
269
-
270
- while next_running_date in queue[sch_wf.alias]:
271
- next_running_date = runner.next
272
-
273
- workflow_tasks.append(
274
- WorkflowTaskData(
275
- alias=sch_wf.alias,
276
- workflow=wf,
277
- runner=runner,
278
- params=sch_wf.values,
279
- ),
280
- )
307
+ workflow_tasks.extend(
308
+ workflow.tasks(start_date, queue=queue, externals=externals)
309
+ )
281
310
 
282
311
  return workflow_tasks
283
312
 
@@ -298,14 +327,6 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
298
327
 
299
328
  def decorator(func: ReturnCancelJob) -> ReturnCancelJob: # pragma: no cov
300
329
  try:
301
- # NOTE: Check the function that want to handle is method or not.
302
- if inspect.ismethod(func):
303
-
304
- @wraps(func)
305
- def wrapper(self, *args, **kwargs):
306
- return func(self, *args, **kwargs)
307
-
308
- return wrapper
309
330
 
310
331
  @wraps(func)
311
332
  def wrapper(*args, **kwargs):
@@ -322,37 +343,37 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
322
343
  return decorator
323
344
 
324
345
 
325
- @catch_exceptions(cancel_on_failure=True) # pragma: no cov
326
- def workflow_task_release(
327
- workflow_tasks: list[WorkflowTaskData],
346
+ class ReleaseThread(TypedDict):
347
+ thread: Thread
348
+ start_date: datetime
349
+
350
+
351
+ ReleaseThreads = dict[str, ReleaseThread]
352
+
353
+
354
+ @catch_exceptions(cancel_on_failure=True)
355
+ def schedule_task(
356
+ tasks: list[WorkflowTask],
328
357
  stop: datetime,
329
- queue,
330
- running,
331
- threads: dict[str, Thread],
358
+ queue: dict[str, WorkflowQueue],
359
+ threads: ReleaseThreads,
360
+ log: type[Log],
332
361
  ) -> CancelJob | None:
333
362
  """Workflow task generator that create release pair of workflow and on to
334
363
  the threading in background.
335
364
 
336
365
  This workflow task will start every minute at ':02' second.
337
366
 
338
- :param workflow_tasks:
367
+ :param tasks: A list of WorkflowTask object.
339
368
  :param stop: A stop datetime object that force stop running scheduler.
340
- :param queue:
341
- :param running:
342
- :param threads:
369
+ :param queue: A mapping of alias name and WorkflowQueue object.
370
+ :param threads: A mapping of alias name and Thread object.
371
+ :param log: A log class that want to making log object.
372
+
343
373
  :rtype: CancelJob | None
344
374
  """
345
375
  current_date: datetime = datetime.now(tz=config.tz)
346
-
347
376
  if current_date > stop.replace(tzinfo=config.tz):
348
- logger.info("[WORKFLOW]: Stop this schedule with datetime stopper.")
349
- while len(threads) > 0:
350
- logger.warning(
351
- "[WORKFLOW]: Waiting workflow release thread that still "
352
- "running in background."
353
- )
354
- time.sleep(15)
355
- workflow_monitor(threads)
356
377
  return CancelJob
357
378
 
358
379
  # IMPORTANT:
@@ -367,91 +388,104 @@ def workflow_task_release(
367
388
  # '00:02:00' --> '*/2 * * * *' --> running
368
389
  # --> '*/35 * * * *' --> skip
369
390
  #
370
- for task in workflow_tasks:
391
+ for task in tasks:
392
+
393
+ q: WorkflowQueue = queue[task.alias]
394
+
395
+ # NOTE: Start adding queue and move the runner date in the WorkflowTask.
396
+ task.queue(stop, q, log=log)
371
397
 
372
398
  # NOTE: Get incoming datetime queue.
373
- logger.debug(
374
- f"[WORKFLOW]: Current queue: {task.workflow.name!r} : "
375
- f"{list(queue2str(queue[task.alias]))}"
376
- )
399
+ logger.debug(f"[WORKFLOW]: Queue: {task.alias!r} : {list(q.queue)}")
377
400
 
378
- if (
379
- len(queue[task.alias]) > 0
380
- and task.runner.date != queue[task.alias][0]
381
- ):
382
- logger.debug(
383
- f"[WORKFLOW]: Skip schedule "
384
- f"{task.runner.date:%Y-%m-%d %H:%M:%S} "
385
- f"for : {task.workflow.name!r} : {task.runner.cron}"
401
+ # VALIDATE: Check the queue is empty or not.
402
+ if not q.is_queued:
403
+ logger.warning(
404
+ f"[WORKFLOW]: Queue is empty for : {task.alias!r} : "
405
+ f"{task.runner.cron}"
386
406
  )
387
407
  continue
388
408
 
389
- elif len(queue[task.alias]) == 0:
390
- logger.warning(
391
- f"[WORKFLOW]: Queue is empty for : {task.workflow.name!r} : "
392
- f"{task.runner.cron}"
409
+ # VALIDATE: Check this task is the first release in the queue or not.
410
+ current_release: datetime = current_date.replace(
411
+ second=0, microsecond=0
412
+ )
413
+ if (first_date := q.first_queue.date) != current_release:
414
+ logger.debug(
415
+ f"[WORKFLOW]: Skip schedule "
416
+ f"{first_date:%Y-%m-%d %H:%M:%S} for : {task.alias!r}"
393
417
  )
394
418
  continue
395
419
 
396
- # NOTE: Remove this datetime from queue.
397
- queue[task.alias].pop(0)
420
+ # NOTE: Pop the latest release and push it to running.
421
+ release: WorkflowRelease = heappop(q.queue)
422
+ heappush(q.running, release)
398
423
 
399
- # NOTE: Create thread name that able to tracking with observe schedule
400
- # job.
401
- thread_name: str = (
402
- f"{task.workflow.name}|{str(task.runner.cron)}|"
403
- f"{task.runner.date:%Y%m%d%H%M}"
424
+ logger.info(
425
+ f"[WORKFLOW]: Start thread: '{task.alias}|"
426
+ f"{release.date:%Y%m%d%H%M}'"
404
427
  )
405
428
 
406
- wf_thread: Thread = Thread(
429
+ # NOTE: Create thread name that able to tracking with observe schedule
430
+ # job.
431
+ thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
432
+ thread: Thread = Thread(
407
433
  target=catch_exceptions(cancel_on_failure=True)(task.release),
408
- kwargs={
409
- "queue": queue,
410
- "running": running,
411
- },
434
+ kwargs={"release": release, "queue": q, "log": log},
412
435
  name=thread_name,
413
436
  daemon=True,
414
437
  )
415
438
 
416
- threads[thread_name] = wf_thread
439
+ threads[thread_name] = {
440
+ "thread": thread,
441
+ "start_date": datetime.now(tz=config.tz),
442
+ }
417
443
 
418
- wf_thread.start()
444
+ thread.start()
419
445
 
420
446
  delay()
421
447
 
422
- logger.debug(f"[WORKFLOW]: {'=' * 100}")
448
+ logger.debug(f"[SCHEDULE]: End schedule release {'=' * 80}")
423
449
 
424
450
 
425
- def workflow_monitor(threads: dict[str, Thread]) -> None: # pragma: no cov
426
- """Workflow schedule for monitoring long running thread from the schedule
427
- control.
451
+ def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
452
+ """Monitoring function that running every five minute for track long running
453
+ thread instance from the schedule_control function that run every minute.
428
454
 
429
455
  :param threads: A mapping of Thread object and its name.
430
- :rtype: None
456
+ :type threads: ReleaseThreads
431
457
  """
432
458
  logger.debug(
433
459
  "[MONITOR]: Start checking long running workflow release task."
434
460
  )
435
- snapshot_threads = list(threads.keys())
461
+
462
+ snapshot_threads: list[str] = list(threads.keys())
436
463
  for t_name in snapshot_threads:
437
464
 
465
+ thread_release: ReleaseThread = threads[t_name]
466
+
438
467
  # NOTE: remove the thread that running success.
439
- if not threads[t_name].is_alive():
468
+ if not thread_release["thread"].is_alive():
440
469
  threads.pop(t_name)
441
470
 
442
471
 
443
- def workflow_control(
472
+ def schedule_control(
444
473
  schedules: list[str],
445
474
  stop: datetime | None = None,
446
475
  externals: DictData | None = None,
476
+ *,
477
+ log: type[Log] | None = None,
447
478
  ) -> list[str]: # pragma: no cov
448
- """Workflow scheduler control.
479
+ """Scheduler control function that running every minute.
449
480
 
450
481
  :param schedules: A list of workflow names that want to schedule running.
451
482
  :param stop: An datetime value that use to stop running schedule.
452
483
  :param externals: An external parameters that pass to Loader.
484
+ :param log:
485
+
453
486
  :rtype: list[str]
454
487
  """
488
+ # NOTE: Lazy import Scheduler object from the schedule package.
455
489
  try:
456
490
  from schedule import Scheduler
457
491
  except ImportError:
@@ -459,30 +493,27 @@ def workflow_control(
459
493
  "Should install schedule package before use this module."
460
494
  ) from None
461
495
 
496
+ log: type[Log] = log or FileLog
462
497
  scheduler: Scheduler = Scheduler()
463
498
  start_date: datetime = datetime.now(tz=config.tz)
499
+ stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
464
500
 
465
- # NOTE: Design workflow queue caching.
466
- # ---
467
- # {"workflow-name": [<release-datetime>, <release-datetime>, ...]}
468
- #
469
- wf_queue: dict[str, list[datetime]] = {}
470
- thread_releases: dict[str, Thread] = {}
501
+ # IMPORTANT: Create main mapping of queue and thread object.
502
+ queue: dict[str, WorkflowQueue] = {}
503
+ threads: ReleaseThreads = {}
471
504
 
472
- start_date_waiting: datetime = (start_date + timedelta(minutes=1)).replace(
505
+ start_date_waiting: datetime = start_date.replace(
473
506
  second=0, microsecond=0
474
- )
507
+ ) + timedelta(minutes=1)
475
508
 
476
- # NOTE: Create pair of workflow and on from schedule model.
477
- workflow_tasks: list[WorkflowTaskData] = []
509
+ # NOTE: Start create workflow tasks from list of schedule name.
510
+ tasks: list[WorkflowTask] = []
478
511
  for name in schedules:
479
512
  schedule: Schedule = Schedule.from_loader(name, externals=externals)
480
-
481
- # NOTE: Create a workflow task data instance from schedule object.
482
- workflow_tasks.extend(
513
+ tasks.extend(
483
514
  schedule.tasks(
484
515
  start_date_waiting,
485
- queue=wf_queue,
516
+ queue=queue,
486
517
  externals=externals,
487
518
  ),
488
519
  )
@@ -492,23 +523,33 @@ def workflow_control(
492
523
  scheduler.every(1)
493
524
  .minutes.at(":02")
494
525
  .do(
495
- workflow_task_release,
496
- workflow_tasks=workflow_tasks,
497
- stop=(stop or (start_date + config.stop_boundary_delta)),
498
- queue=wf_queue,
499
- threads=thread_releases,
526
+ schedule_task,
527
+ tasks=tasks,
528
+ stop=stop_date,
529
+ queue=queue,
530
+ threads=threads,
531
+ log=log,
500
532
  )
501
533
  .tag("control")
502
534
  )
503
535
 
504
536
  # NOTE: Checking zombie task with schedule job will start every 5 minute.
505
- scheduler.every(5).minutes.at(":10").do(
506
- workflow_monitor,
507
- threads=thread_releases,
508
- ).tag("monitor")
537
+ (
538
+ scheduler.every(5)
539
+ .minutes.at(":10")
540
+ .do(
541
+ monitor,
542
+ threads=threads,
543
+ )
544
+ .tag("monitor")
545
+ )
509
546
 
510
547
  # NOTE: Start running schedule
511
- logger.info(f"[WORKFLOW]: Start schedule: {schedules}")
548
+ logger.info(
549
+ f"[SCHEDULE]: Schedule: {schedules} with stopper: "
550
+ f"{stop_date:%Y-%m-%d %H:%M:%S}"
551
+ )
552
+
512
553
  while True:
513
554
  scheduler.run_pending()
514
555
  time.sleep(1)
@@ -516,29 +557,35 @@ def workflow_control(
516
557
  # NOTE: Break the scheduler when the control job does not exists.
517
558
  if not scheduler.get_jobs("control"):
518
559
  scheduler.clear("monitor")
519
- logger.warning(
520
- f"[WORKFLOW]: Workflow release thread: {thread_releases}"
521
- )
522
- logger.warning("[WORKFLOW]: Does not have any schedule jobs !!!")
560
+
561
+ while len(threads) > 0:
562
+ logger.warning(
563
+ "[SCHEDULE]: Waiting schedule release thread that still "
564
+ "running in background."
565
+ )
566
+ delay(15)
567
+ monitor(threads)
568
+
523
569
  break
524
570
 
525
571
  logger.warning(
526
- f"Queue: {[list(queue2str(wf_queue[wf])) for wf in wf_queue]}"
572
+ f"[SCHEDULE]: Queue: {[list(queue[wf].queue) for wf in queue]}"
527
573
  )
528
574
  return schedules
529
575
 
530
576
 
531
- def workflow_runner(
577
+ def schedule_runner(
532
578
  stop: datetime | None = None,
533
579
  externals: DictData | None = None,
534
580
  excluded: list[str] | None = None,
535
581
  ) -> list[str]: # pragma: no cov
536
- """Workflow application that running multiprocessing schedule with chunk of
537
- workflows that exists in config path.
582
+ """Schedule runner function for start submit the ``schedule_control`` func
583
+ in multiprocessing pool with chunk of schedule config that exists in config
584
+ path by ``WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS``.
538
585
 
539
586
  :param stop: A stop datetime object that force stop running scheduler.
540
- :param excluded:
541
587
  :param externals:
588
+ :param excluded: A list of schedule name that want to excluded from finding.
542
589
 
543
590
  :rtype: list[str]
544
591
 
@@ -549,24 +596,21 @@ def workflow_runner(
549
596
 
550
597
  The current workflow logic that split to process will be below diagram:
551
598
 
552
- PIPELINES ==> process 01 ==> schedule --> thread of release
553
- workflow task 01 01
554
- --> thread of release
555
- workflow task 01 02
556
- ==> process 02 ==> schedule --> thread of release
557
- workflow task 02 01
558
- --> thread of release
559
- workflow task 02 02
560
- ==> ...
599
+ MAIN ==> process 01 ==> schedule --> thread of release task 01 01
600
+ --> thread of release task 01 02
601
+ ==> schedule --> thread of release task 02 01
602
+ --> thread of release task 02 02
603
+ ==> process 02
561
604
  """
562
- excluded: list[str] = excluded or []
605
+ results: list[str] = []
563
606
 
564
607
  with ProcessPoolExecutor(
565
608
  max_workers=config.max_schedule_process,
566
609
  ) as executor:
610
+
567
611
  futures: list[Future] = [
568
612
  executor.submit(
569
- workflow_control,
613
+ schedule_control,
570
614
  schedules=[load[0] for load in loader],
571
615
  stop=stop,
572
616
  externals=(externals or {}),
@@ -577,10 +621,13 @@ def workflow_runner(
577
621
  )
578
622
  ]
579
623
 
580
- results: list[str] = []
581
624
  for future in as_completed(futures):
625
+
626
+ # NOTE: Raise error when it has any error from schedule_control.
582
627
  if err := future.exception():
583
628
  logger.error(str(err))
584
629
  raise WorkflowException(str(err)) from err
630
+
585
631
  results.extend(future.result(timeout=1))
586
- return results
632
+
633
+ return results
ddeutil/workflow/stage.py CHANGED
@@ -346,6 +346,11 @@ class EmptyStage(BaseStage):
346
346
  f"( {param2template(self.echo, params=params) or '...'} )"
347
347
  )
348
348
  if self.sleep > 0:
349
+ if self.sleep > 30:
350
+ logger.info(
351
+ f"({cut_id(run_id)}) [STAGE]: ... sleep "
352
+ f"({self.sleep} seconds)"
353
+ )
349
354
  time.sleep(self.sleep)
350
355
  return Result(status=0, context={}, run_id=run_id)
351
356
 
ddeutil/workflow/utils.py CHANGED
@@ -74,6 +74,12 @@ def get_diff_sec(
74
74
  )
75
75
 
76
76
 
77
+ def wait_a_minute(now: datetime, second: float = 2) -> None: # pragma: no cov
78
+ """Wait with sleep to the next minute with an offset second value."""
79
+ future = now.replace(second=0, microsecond=0) + timedelta(minutes=1)
80
+ time.sleep((future - now).total_seconds() + second)
81
+
82
+
77
83
  def delay(second: float = 0) -> None: # pragma: no cov
78
84
  """Delay time that use time.sleep with random second value between
79
85
  0.00 - 0.99 seconds.
@@ -326,7 +332,7 @@ def get_args_from_filter(
326
332
 
327
333
  if func_name not in filters:
328
334
  raise UtilException(
329
- f"The post-filter: {func_name} does not support yet."
335
+ f"The post-filter: {func_name!r} does not support yet."
330
336
  )
331
337
 
332
338
  if isinstance((f_func := filters[func_name]), list) and (args or kwargs):
@@ -577,10 +583,6 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
577
583
  yield chain((first_el,), chunk_it)
578
584
 
579
585
 
580
- def queue2str(queue: list[datetime]) -> Iterator[str]: # pragma: no cov
581
- return (f"{q:%Y-%m-%d %H:%M:%S}" for q in queue)
582
-
583
-
584
586
  def cut_id(run_id: str, *, num: int = 6):
585
587
  """Cutting running ID with length.
586
588