ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.74__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.
@@ -3,7 +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 fix config for `set_logging`, and Model initialize step.
6
+ """Tracing and Logging Module for Workflow Execution.
7
+
8
+ This module provides comprehensive tracing and logging capabilities for workflow
9
+ execution monitoring. It supports multiple trace backends including console output,
10
+ file-based logging, and SQLite database storage.
11
+
12
+ The tracing system captures detailed execution metadata including process IDs,
13
+ thread identifiers, timestamps, and contextual information for debugging and
14
+ monitoring workflow executions.
15
+
16
+ Classes:
17
+ Message: Log message model with prefix parsing
18
+ TraceMeta: Metadata model for execution context
19
+ TraceData: Container for trace information
20
+ BaseTrace: Abstract base class for trace implementations
21
+ ConsoleTrace: Console-based trace output
22
+ FileTrace: File-based trace storage
23
+ SQLiteTrace: Database-based trace storage
24
+
25
+ Functions:
26
+ set_logging: Configure logger with custom formatting
27
+ get_trace: Factory function for trace instances
28
+
29
+ Example:
30
+ >>> from ddeutil.workflow.traces import get_trace
31
+ >>> # Create file-based trace
32
+ >>> trace = get_trace("running-id-101", parent_run_id="workflow-001")
33
+ >>> trace.info("Workflow execution started")
34
+ >>> trace.debug("Processing stage 1")
35
+ """
7
36
  from __future__ import annotations
8
37
 
9
38
  import json
@@ -17,9 +46,12 @@ from inspect import Traceback, currentframe, getframeinfo
17
46
  from pathlib import Path
18
47
  from threading import get_ident
19
48
  from types import FrameType
20
- from typing import ClassVar, Final, Literal, Optional, TypeVar, Union
49
+ from typing import ClassVar, Final, Literal, Optional, Union
50
+ from urllib.parse import ParseResult, unquote_plus, urlparse
21
51
 
22
52
  from pydantic import BaseModel, ConfigDict, Field
53
+ from pydantic.functional_serializers import field_serializer
54
+ from pydantic.functional_validators import field_validator
23
55
  from typing_extensions import Self
24
56
 
25
57
  from .__types import DictData
@@ -32,12 +64,23 @@ logger = logging.getLogger("ddeutil.workflow")
32
64
 
33
65
  @lru_cache
34
66
  def set_logging(name: str) -> logging.Logger:
35
- """Return logger object with an input module name that already implement the
36
- custom handler and formatter from this package config.
67
+ """Configure logger with custom formatting and handlers.
37
68
 
38
- :param name: (str) A module name that want to log.
69
+ Creates and configures a logger instance with the custom formatter and
70
+ handlers defined in the package configuration. The logger includes both
71
+ console output and proper formatting for workflow execution tracking.
39
72
 
40
- :rtype: logging.Logger
73
+ Args:
74
+ name: Module name to create logger for
75
+
76
+ Returns:
77
+ logging.Logger: Configured logger instance with custom formatting
78
+
79
+ Example:
80
+ ```python
81
+ logger = set_logging("ddeutil.workflow.stages")
82
+ logger.info("Stage execution started")
83
+ ```
41
84
  """
42
85
  _logger = logging.getLogger(name)
43
86
 
@@ -101,10 +144,12 @@ class Message(BaseModel):
101
144
  def prepare(self, extras: Optional[DictData] = None) -> str:
102
145
  """Prepare message with force add prefix before writing trace log.
103
146
 
104
- :param extras: (DictData) An extra parameter that want to get the
105
- `log_add_emoji` flag.
147
+ Args:
148
+ extras: An extra parameter that want to get the
149
+ `log_add_emoji` flag.
106
150
 
107
- :rtype: str
151
+ Returns:
152
+ str: The prepared message with prefix and optional emoji.
108
153
  """
109
154
  name: str = self.name or PREFIX_DEFAULT
110
155
  emoji: str = (
@@ -139,9 +184,13 @@ class TraceMeta(BaseModel): # pragma: no cov
139
184
  """Dynamic Frame information base on the `logs_trace_frame_layer` config
140
185
  value that was set from the extra parameter.
141
186
 
142
- :param frame: (FrameType) The current frame that want to dynamic.
143
- :param extras: (DictData) An extra parameter that want to get the
144
- `logs_trace_frame_layer` config value.
187
+ Args:
188
+ frame: The current frame that want to dynamic.
189
+ extras: An extra parameter that want to get the
190
+ `logs_trace_frame_layer` config value.
191
+
192
+ Returns:
193
+ Traceback: The frame information at the specified layer.
145
194
  """
146
195
  extras: DictData = extras or {}
147
196
  layer: int = extras.get("logs_trace_frame_layer", 4)
@@ -167,14 +216,16 @@ class TraceMeta(BaseModel): # pragma: no cov
167
216
  """Make the current metric for contract this TraceMeta model instance
168
217
  that will catch local states like PID, thread identity.
169
218
 
170
- :param mode: (Literal["stdout", "stderr"]) A metadata mode.
171
- :param message: (str) A message.
172
- :param level: (str) A log level.
173
- :param cutting_id: (str)
174
- :param extras: (DictData) An extra parameter that want to override core
175
- config values.
219
+ Args:
220
+ mode: A metadata mode.
221
+ message: A message.
222
+ level: A log level.
223
+ cutting_id: A cutting ID string.
224
+ extras: An extra parameter that want to override core
225
+ config values.
176
226
 
177
- :rtype: Self
227
+ Returns:
228
+ Self: The constructed TraceMeta instance.
178
229
  """
179
230
  frame: FrameType = currentframe()
180
231
  frame_info: Traceback = cls.dynamic_frame(frame, extras=extras)
@@ -252,43 +303,6 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
252
303
  description="A parent running ID",
253
304
  )
254
305
 
255
- @classmethod
256
- @abstractmethod
257
- def find_traces(
258
- cls,
259
- path: Optional[Path] = None,
260
- extras: Optional[DictData] = None,
261
- ) -> Iterator[TraceData]: # pragma: no cov
262
- """Return iterator of TraceData models from the target pointer.
263
-
264
- Args:
265
- path (:obj:`Path`, optional): A pointer path that want to override.
266
- extras (:obj:`DictData`, optional): An extras parameter that want to
267
- override default engine config.
268
-
269
- Returns:
270
- Iterator[TracData]: An iterator object that generate a TracData
271
- model.
272
- """
273
- raise NotImplementedError(
274
- "Trace dataclass should implement `find_traces` class-method."
275
- )
276
-
277
- @classmethod
278
- @abstractmethod
279
- def find_trace_with_id(
280
- cls,
281
- run_id: str,
282
- force_raise: bool = True,
283
- *,
284
- path: Optional[Path] = None,
285
- extras: Optional[DictData] = None,
286
- ) -> TraceData:
287
- raise NotImplementedError(
288
- "Trace dataclass should implement `find_trace_with_id` "
289
- "class-method."
290
- )
291
-
292
306
  @abstractmethod
293
307
  def writer(
294
308
  self,
@@ -340,7 +354,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
340
354
  )
341
355
 
342
356
  @abstractmethod
343
- def _logging(
357
+ def emit(
344
358
  self,
345
359
  message: str,
346
360
  mode: str,
@@ -364,7 +378,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
364
378
 
365
379
  :param message: (str) A message that want to log.
366
380
  """
367
- self._logging(message, mode="debug")
381
+ self.emit(message, mode="debug")
368
382
 
369
383
  def info(self, message: str) -> None:
370
384
  """Write trace log with append mode and logging this message with the
@@ -372,7 +386,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
372
386
 
373
387
  :param message: (str) A message that want to log.
374
388
  """
375
- self._logging(message, mode="info")
389
+ self.emit(message, mode="info")
376
390
 
377
391
  def warning(self, message: str) -> None:
378
392
  """Write trace log with append mode and logging this message with the
@@ -380,7 +394,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
380
394
 
381
395
  :param message: (str) A message that want to log.
382
396
  """
383
- self._logging(message, mode="warning")
397
+ self.emit(message, mode="warning")
384
398
 
385
399
  def error(self, message: str) -> None:
386
400
  """Write trace log with append mode and logging this message with the
@@ -388,7 +402,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
388
402
 
389
403
  :param message: (str) A message that want to log.
390
404
  """
391
- self._logging(message, mode="error", is_err=True)
405
+ self.emit(message, mode="error", is_err=True)
392
406
 
393
407
  def exception(self, message: str) -> None:
394
408
  """Write trace log with append mode and logging this message with the
@@ -396,10 +410,10 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
396
410
 
397
411
  :param message: (str) A message that want to log.
398
412
  """
399
- self._logging(message, mode="exception", is_err=True)
413
+ self.emit(message, mode="exception", is_err=True)
400
414
 
401
415
  @abstractmethod
402
- async def _alogging(
416
+ async def amit(
403
417
  self,
404
418
  message: str,
405
419
  mode: str,
@@ -423,7 +437,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
423
437
 
424
438
  :param message: (str) A message that want to log.
425
439
  """
426
- await self._alogging(message, mode="debug")
440
+ await self.amit(message, mode="debug")
427
441
 
428
442
  async def ainfo(self, message: str) -> None: # pragma: no cov
429
443
  """Async write trace log with append mode and logging this message with
@@ -431,7 +445,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
431
445
 
432
446
  :param message: (str) A message that want to log.
433
447
  """
434
- await self._alogging(message, mode="info")
448
+ await self.amit(message, mode="info")
435
449
 
436
450
  async def awarning(self, message: str) -> None: # pragma: no cov
437
451
  """Async write trace log with append mode and logging this message with
@@ -439,7 +453,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
439
453
 
440
454
  :param message: (str) A message that want to log.
441
455
  """
442
- await self._alogging(message, mode="warning")
456
+ await self.amit(message, mode="warning")
443
457
 
444
458
  async def aerror(self, message: str) -> None: # pragma: no cov
445
459
  """Async write trace log with append mode and logging this message with
@@ -447,7 +461,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
447
461
 
448
462
  :param message: (str) A message that want to log.
449
463
  """
450
- await self._alogging(message, mode="error", is_err=True)
464
+ await self.amit(message, mode="error", is_err=True)
451
465
 
452
466
  async def aexception(self, message: str) -> None: # pragma: no cov
453
467
  """Async write trace log with append mode and logging this message with
@@ -455,36 +469,12 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
455
469
 
456
470
  :param message: (str) A message that want to log.
457
471
  """
458
- await self._alogging(message, mode="exception", is_err=True)
472
+ await self.amit(message, mode="exception", is_err=True)
459
473
 
460
474
 
461
475
  class ConsoleTrace(BaseTrace): # pragma: no cov
462
476
  """Console Trace log model."""
463
477
 
464
- @classmethod
465
- def find_traces(
466
- cls,
467
- path: Optional[Path] = None,
468
- extras: Optional[DictData] = None,
469
- ) -> Iterator[TraceData]: # pragma: no cov
470
- raise NotImplementedError(
471
- "Console Trace does not support to find history traces data."
472
- )
473
-
474
- @classmethod
475
- def find_trace_with_id(
476
- cls,
477
- run_id: str,
478
- force_raise: bool = True,
479
- *,
480
- path: Optional[Path] = None,
481
- extras: Optional[DictData] = None,
482
- ) -> TraceData:
483
- raise NotImplementedError(
484
- "Console Trace does not support to find history traces data with "
485
- "the specific running ID."
486
- )
487
-
488
478
  def writer(
489
479
  self,
490
480
  message: str,
@@ -537,13 +527,13 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
537
527
  """
538
528
  return prepare_newline(Message.from_str(message).prepare(self.extras))
539
529
 
540
- def _logging(
541
- self, message: str, mode: str, *, is_err: bool = False
542
- ) -> None:
530
+ def emit(self, message: str, mode: str, *, is_err: bool = False) -> None:
543
531
  """Write trace log with append mode and logging this message with any
544
532
  logging level.
545
533
 
546
534
  :param message: (str) A message that want to log.
535
+ :param mode: (str)
536
+ :param is_err: (bool)
547
537
  """
548
538
  msg: str = self.make_message(message)
549
539
 
@@ -554,13 +544,15 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
554
544
 
555
545
  getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
556
546
 
557
- async def _alogging(
547
+ async def amit(
558
548
  self, message: str, mode: str, *, is_err: bool = False
559
549
  ) -> None:
560
550
  """Write trace log with append mode and logging this message with any
561
551
  logging level.
562
552
 
563
553
  :param message: (str) A message that want to log.
554
+ :param mode: (str)
555
+ :param is_err: (bool)
564
556
  """
565
557
  msg: str = self.make_message(message)
566
558
 
@@ -572,7 +564,62 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
572
564
  getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
573
565
 
574
566
 
575
- class FileTrace(ConsoleTrace): # pragma: no cov
567
+ class OutsideTrace(ConsoleTrace, ABC):
568
+ model_config = ConfigDict(arbitrary_types_allowed=True)
569
+
570
+ url: ParseResult = Field(description="An URL for create pointer.")
571
+
572
+ @field_validator(
573
+ "url", mode="before", json_schema_input_type=Union[ParseResult, str]
574
+ )
575
+ def __parse_url(cls, value: Union[ParseResult, str]) -> ParseResult:
576
+ if isinstance(value, str):
577
+ return urlparse(value)
578
+ return value
579
+
580
+ @field_serializer("url")
581
+ def __serialize_url(self, value: ParseResult) -> str:
582
+ return value.geturl()
583
+
584
+ @classmethod
585
+ @abstractmethod
586
+ def find_traces(
587
+ cls,
588
+ path: Optional[Path] = None,
589
+ extras: Optional[DictData] = None,
590
+ ) -> Iterator[TraceData]: # pragma: no cov
591
+ """Return iterator of TraceData models from the target pointer.
592
+
593
+ Args:
594
+ path (:obj:`Path`, optional): A pointer path that want to override.
595
+ extras (:obj:`DictData`, optional): An extras parameter that want to
596
+ override default engine config.
597
+
598
+ Returns:
599
+ Iterator[TracData]: An iterator object that generate a TracData
600
+ model.
601
+ """
602
+ raise NotImplementedError(
603
+ "Trace dataclass should implement `find_traces` class-method."
604
+ )
605
+
606
+ @classmethod
607
+ @abstractmethod
608
+ def find_trace_with_id(
609
+ cls,
610
+ run_id: str,
611
+ force_raise: bool = True,
612
+ *,
613
+ path: Optional[Path] = None,
614
+ extras: Optional[DictData] = None,
615
+ ) -> TraceData:
616
+ raise NotImplementedError(
617
+ "Trace dataclass should implement `find_trace_with_id` "
618
+ "class-method."
619
+ )
620
+
621
+
622
+ class FileTrace(OutsideTrace): # pragma: no cov
576
623
  """File Trace dataclass that write file to the local storage."""
577
624
 
578
625
  @classmethod
@@ -587,7 +634,9 @@ class FileTrace(ConsoleTrace): # pragma: no cov
587
634
  :param extras: An extra parameter that want to override core config.
588
635
  """
589
636
  for file in sorted(
590
- (path or dynamic("trace_path", extras=extras)).glob("./run_id=*"),
637
+ (path or Path(dynamic("trace_url", extras=extras).path)).glob(
638
+ "./run_id=*"
639
+ ),
591
640
  key=lambda f: f.lstat().st_mtime,
592
641
  ):
593
642
  yield TraceData.from_path(file)
@@ -608,7 +657,7 @@ class FileTrace(ConsoleTrace): # pragma: no cov
608
657
  :param path: (Path)
609
658
  :param extras: An extra parameter that want to override core config.
610
659
  """
611
- base_path: Path = path or dynamic("trace_path", extras=extras)
660
+ base_path: Path = path or Path(dynamic("trace_url", extras=extras).path)
612
661
  file: Path = base_path / f"run_id={run_id}"
613
662
  if file.exists():
614
663
  return TraceData.from_path(file)
@@ -624,10 +673,14 @@ class FileTrace(ConsoleTrace): # pragma: no cov
624
673
  """Pointer of the target path that use to writing trace log or searching
625
674
  trace log.
626
675
 
676
+ This running ID folder that use to keeping trace log data will use
677
+ a parent running ID first. If it does not set, it will use running ID
678
+ instead.
679
+
627
680
  :rtype: Path
628
681
  """
629
682
  log_file: Path = (
630
- dynamic("trace_path", extras=self.extras)
683
+ Path(unquote_plus(self.url.path))
631
684
  / f"run_id={self.parent_run_id or self.run_id}"
632
685
  )
633
686
  if not log_file.exists():
@@ -710,17 +763,20 @@ class FileTrace(ConsoleTrace): # pragma: no cov
710
763
  await f.write(trace_meta.model_dump_json() + "\n")
711
764
 
712
765
 
713
- class SQLiteTrace(ConsoleTrace): # pragma: no cov
766
+ class SQLiteTrace(OutsideTrace): # pragma: no cov
714
767
  """SQLite Trace dataclass that write trace log to the SQLite database file."""
715
768
 
716
769
  table_name: ClassVar[str] = "audits"
717
770
  schemas: ClassVar[
718
771
  str
719
772
  ] = """
720
- run_id int,
721
- stdout str,
722
- stderr str,
723
- update datetime
773
+ run_id str
774
+ , parent_run_id str
775
+ , type str
776
+ , text str
777
+ , metadata JSON
778
+ , created_at datetime
779
+ , updated_at datetime
724
780
  primary key ( run_id )
725
781
  """
726
782
 
@@ -729,7 +785,8 @@ class SQLiteTrace(ConsoleTrace): # pragma: no cov
729
785
  cls,
730
786
  path: Optional[Path] = None,
731
787
  extras: Optional[DictData] = None,
732
- ) -> Iterator[TraceData]: ...
788
+ ) -> Iterator[TraceData]:
789
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
733
790
 
734
791
  @classmethod
735
792
  def find_trace_with_id(
@@ -739,30 +796,33 @@ class SQLiteTrace(ConsoleTrace): # pragma: no cov
739
796
  *,
740
797
  path: Optional[Path] = None,
741
798
  extras: Optional[DictData] = None,
742
- ) -> TraceData: ...
799
+ ) -> TraceData:
800
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
743
801
 
744
- def make_message(self, message: str) -> str: ...
802
+ def make_message(self, message: str) -> str:
803
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
745
804
 
746
805
  def writer(
747
806
  self,
748
807
  message: str,
749
808
  level: str,
750
809
  is_err: bool = False,
751
- ) -> None: ...
810
+ ) -> None:
811
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
752
812
 
753
813
  def awriter(
754
814
  self,
755
815
  message: str,
756
816
  level: str,
757
817
  is_err: bool = False,
758
- ) -> None: ...
818
+ ) -> None:
819
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
759
820
 
760
821
 
761
- Trace = TypeVar("Trace", bound=BaseTrace)
762
- TraceModel = Union[
763
- ConsoleTrace,
822
+ Trace = Union[
764
823
  FileTrace,
765
824
  SQLiteTrace,
825
+ OutsideTrace,
766
826
  ]
767
827
 
768
828
 
@@ -771,7 +831,7 @@ def get_trace(
771
831
  *,
772
832
  parent_run_id: Optional[str] = None,
773
833
  extras: Optional[DictData] = None,
774
- ) -> TraceModel: # pragma: no cov
834
+ ) -> Trace: # pragma: no cov
775
835
  """Get dynamic Trace instance from the core config (it can override by an
776
836
  extras argument) that passing running ID and parent running ID.
777
837
 
@@ -780,12 +840,31 @@ def get_trace(
780
840
  :param extras: (DictData) An extra parameter that want to override the core
781
841
  config values.
782
842
 
783
- :rtype: TraceLog
843
+ :rtype: Trace
784
844
  """
785
- if dynamic("trace_path", extras=extras).is_file():
786
- return SQLiteTrace(
787
- run_id=run_id, parent_run_id=parent_run_id, extras=(extras or {})
845
+ # NOTE: Allow you to override trace model by the extra parameter.
846
+ map_trace_models: dict[str, type[Trace]] = extras.get(
847
+ "trace_model_mapping", {}
848
+ )
849
+ url: ParseResult
850
+ if (url := dynamic("trace_url", extras=extras)).scheme and (
851
+ url.scheme == "sqlite"
852
+ or (url.scheme == "file" and Path(url.path).is_file())
853
+ ):
854
+ return map_trace_models.get("sqlite", SQLiteTrace)(
855
+ url=url,
856
+ run_id=run_id,
857
+ parent_run_id=parent_run_id,
858
+ extras=(extras or {}),
859
+ )
860
+ elif url.scheme and url.scheme != "file":
861
+ raise NotImplementedError(
862
+ f"Does not implement the outside trace model support for URL: {url}"
788
863
  )
789
- return FileTrace(
790
- run_id=run_id, parent_run_id=parent_run_id, extras=(extras or {})
864
+
865
+ return map_trace_models.get("file", FileTrace)(
866
+ url=url,
867
+ run_id=run_id,
868
+ parent_run_id=parent_run_id,
869
+ extras=(extras or {}),
791
870
  )
ddeutil/workflow/utils.py CHANGED
@@ -3,7 +3,35 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Utility function model."""
6
+ """Utility Functions for Workflow Operations.
7
+
8
+ This module provides essential utility functions used throughout the workflow
9
+ system for ID generation, datetime handling, string processing, template
10
+ operations, and other common tasks.
11
+
12
+ Functions:
13
+ gen_id: Generate unique identifiers for workflow components
14
+ make_exec: Create executable strings for shell commands
15
+ filter_func: Filter functions based on criteria
16
+ dump_all: Serialize data to various formats
17
+ delay: Create delays in execution
18
+ to_train: Convert strings to train-case format
19
+ get_dt_now: Get current datetime with timezone
20
+ get_d_now: Get current date
21
+ cross_product: Generate cross product of matrix values
22
+ replace_sec: Replace template variables in strings
23
+
24
+ Example:
25
+ ```python
26
+ from ddeutil.workflow.utils import gen_id, get_dt_now
27
+
28
+ # Generate unique ID
29
+ run_id = gen_id("workflow")
30
+
31
+ # Get current datetime
32
+ now = get_dt_now()
33
+ ```
34
+ """
7
35
  from __future__ import annotations
8
36
 
9
37
  import stat
@@ -27,13 +55,19 @@ T = TypeVar("T")
27
55
  UTC: Final[ZoneInfo] = ZoneInfo("UTC")
28
56
  MARK_NEWLINE: Final[str] = "||"
29
57
 
58
+ # Cache for random delay values to avoid repeated randrange calls
59
+ _CACHED_DELAYS = [randrange(0, 99, step=10) / 100 for _ in range(100)]
60
+ _DELAY_INDEX = 0
61
+
30
62
 
31
63
  def to_train(camel: str) -> str:
32
64
  """Convert camel case string to train case.
33
65
 
34
- :param camel: (str) A camel case string that want to convert.
66
+ Args:
67
+ camel: A camel case string that want to convert.
35
68
 
36
- :rtype: str
69
+ Returns:
70
+ str: The converted train-case string.
37
71
  """
38
72
  return "".join("-" + i if i.isupper() else i for i in camel).lstrip("-")
39
73
 
@@ -41,9 +75,11 @@ def to_train(camel: str) -> str:
41
75
  def prepare_newline(msg: str) -> str:
42
76
  """Prepare message that has multiple newline char.
43
77
 
44
- :param msg: (str) A message that want to prepare.
78
+ Args:
79
+ msg: A message that want to prepare.
45
80
 
46
- :rtype: str
81
+ Returns:
82
+ str: The prepared message with formatted newlines.
47
83
  """
48
84
  # NOTE: Remove ending with "\n" and replace "\n" with the "||" value.
49
85
  msg: str = msg.strip("\n").replace("\n", MARK_NEWLINE)
@@ -63,9 +99,11 @@ def prepare_newline(msg: str) -> str:
63
99
  def replace_sec(dt: datetime) -> datetime:
64
100
  """Replace second and microsecond values to 0.
65
101
 
66
- :param dt: A datetime object that want to replace.
102
+ Args:
103
+ dt: A datetime object that want to replace.
67
104
 
68
- :rtype: datetime
105
+ Returns:
106
+ datetime: The datetime with seconds and microseconds set to 0.
69
107
  """
70
108
  return dt.replace(second=0, microsecond=0)
71
109
 
@@ -87,6 +125,17 @@ def get_dt_now(tz: Optional[ZoneInfo] = None, offset: float = 0.0) -> datetime:
87
125
  return datetime.now(tz=tz) - timedelta(seconds=offset)
88
126
 
89
127
 
128
+ def get_dt_ntz_now() -> datetime: # pragma: no cov
129
+ """Get current datetime with no timezone.
130
+
131
+ Returns the current datetime object using the None timezone.
132
+
133
+ Returns:
134
+ datetime: Current datetime with no timezone
135
+ """
136
+ return get_dt_now(tz=None)
137
+
138
+
90
139
  def get_d_now(
91
140
  tz: Optional[ZoneInfo] = None, offset: float = 0.0
92
141
  ) -> date: # pragma: no cov
@@ -151,7 +200,10 @@ def delay(second: float = 0) -> None: # pragma: no cov
151
200
 
152
201
  :param second: (float) A second number that want to adds-on random value.
153
202
  """
154
- time.sleep(second + randrange(0, 99, step=10) / 100)
203
+ global _DELAY_INDEX
204
+ cached_random = _CACHED_DELAYS[_DELAY_INDEX % len(_CACHED_DELAYS)]
205
+ _DELAY_INDEX = (_DELAY_INDEX + 1) % len(_CACHED_DELAYS)
206
+ time.sleep(second + cached_random)
155
207
 
156
208
 
157
209
  def gen_id(