ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.75__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 = (
@@ -122,7 +167,9 @@ class TraceMeta(BaseModel): # pragma: no cov
122
167
 
123
168
  mode: Literal["stdout", "stderr"] = Field(description="A meta mode.")
124
169
  level: str = Field(description="A log level.")
125
- datetime: str = Field(description="A datetime in string format.")
170
+ datetime: str = Field(
171
+ description="A datetime string with the specific config format."
172
+ )
126
173
  process: int = Field(description="A process ID.")
127
174
  thread: int = Field(description="A thread ID.")
128
175
  message: str = Field(description="A message log.")
@@ -139,9 +186,13 @@ class TraceMeta(BaseModel): # pragma: no cov
139
186
  """Dynamic Frame information base on the `logs_trace_frame_layer` config
140
187
  value that was set from the extra parameter.
141
188
 
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.
189
+ Args:
190
+ frame: The current frame that want to dynamic.
191
+ extras: An extra parameter that want to get the
192
+ `logs_trace_frame_layer` config value.
193
+
194
+ Returns:
195
+ Traceback: The frame information at the specified layer.
145
196
  """
146
197
  extras: DictData = extras or {}
147
198
  layer: int = extras.get("logs_trace_frame_layer", 4)
@@ -167,14 +218,16 @@ class TraceMeta(BaseModel): # pragma: no cov
167
218
  """Make the current metric for contract this TraceMeta model instance
168
219
  that will catch local states like PID, thread identity.
169
220
 
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.
221
+ Args:
222
+ mode: A metadata mode.
223
+ message: A message.
224
+ level: A log level.
225
+ cutting_id: A cutting ID string.
226
+ extras: An extra parameter that want to override core
227
+ config values.
176
228
 
177
- :rtype: Self
229
+ Returns:
230
+ Self: The constructed TraceMeta instance.
178
231
  """
179
232
  frame: FrameType = currentframe()
180
233
  frame_info: Traceback = cls.dynamic_frame(frame, extras=extras)
@@ -183,9 +236,9 @@ class TraceMeta(BaseModel): # pragma: no cov
183
236
  mode=mode,
184
237
  level=level,
185
238
  datetime=(
186
- get_dt_now(tz=dynamic("tz", extras=extras)).strftime(
187
- dynamic("log_datetime_format", extras=extras)
188
- )
239
+ get_dt_now()
240
+ .astimezone(dynamic("log_tz", extras=extras))
241
+ .strftime(dynamic("log_datetime_format", extras=extras))
189
242
  ),
190
243
  process=os.getpid(),
191
244
  thread=get_ident(),
@@ -252,43 +305,6 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
252
305
  description="A parent running ID",
253
306
  )
254
307
 
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
308
  @abstractmethod
293
309
  def writer(
294
310
  self,
@@ -340,7 +356,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
340
356
  )
341
357
 
342
358
  @abstractmethod
343
- def _logging(
359
+ def emit(
344
360
  self,
345
361
  message: str,
346
362
  mode: str,
@@ -364,7 +380,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
364
380
 
365
381
  :param message: (str) A message that want to log.
366
382
  """
367
- self._logging(message, mode="debug")
383
+ self.emit(message, mode="debug")
368
384
 
369
385
  def info(self, message: str) -> None:
370
386
  """Write trace log with append mode and logging this message with the
@@ -372,7 +388,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
372
388
 
373
389
  :param message: (str) A message that want to log.
374
390
  """
375
- self._logging(message, mode="info")
391
+ self.emit(message, mode="info")
376
392
 
377
393
  def warning(self, message: str) -> None:
378
394
  """Write trace log with append mode and logging this message with the
@@ -380,7 +396,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
380
396
 
381
397
  :param message: (str) A message that want to log.
382
398
  """
383
- self._logging(message, mode="warning")
399
+ self.emit(message, mode="warning")
384
400
 
385
401
  def error(self, message: str) -> None:
386
402
  """Write trace log with append mode and logging this message with the
@@ -388,7 +404,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
388
404
 
389
405
  :param message: (str) A message that want to log.
390
406
  """
391
- self._logging(message, mode="error", is_err=True)
407
+ self.emit(message, mode="error", is_err=True)
392
408
 
393
409
  def exception(self, message: str) -> None:
394
410
  """Write trace log with append mode and logging this message with the
@@ -396,10 +412,10 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
396
412
 
397
413
  :param message: (str) A message that want to log.
398
414
  """
399
- self._logging(message, mode="exception", is_err=True)
415
+ self.emit(message, mode="exception", is_err=True)
400
416
 
401
417
  @abstractmethod
402
- async def _alogging(
418
+ async def amit(
403
419
  self,
404
420
  message: str,
405
421
  mode: str,
@@ -423,7 +439,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
423
439
 
424
440
  :param message: (str) A message that want to log.
425
441
  """
426
- await self._alogging(message, mode="debug")
442
+ await self.amit(message, mode="debug")
427
443
 
428
444
  async def ainfo(self, message: str) -> None: # pragma: no cov
429
445
  """Async write trace log with append mode and logging this message with
@@ -431,7 +447,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
431
447
 
432
448
  :param message: (str) A message that want to log.
433
449
  """
434
- await self._alogging(message, mode="info")
450
+ await self.amit(message, mode="info")
435
451
 
436
452
  async def awarning(self, message: str) -> None: # pragma: no cov
437
453
  """Async write trace log with append mode and logging this message with
@@ -439,7 +455,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
439
455
 
440
456
  :param message: (str) A message that want to log.
441
457
  """
442
- await self._alogging(message, mode="warning")
458
+ await self.amit(message, mode="warning")
443
459
 
444
460
  async def aerror(self, message: str) -> None: # pragma: no cov
445
461
  """Async write trace log with append mode and logging this message with
@@ -447,7 +463,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
447
463
 
448
464
  :param message: (str) A message that want to log.
449
465
  """
450
- await self._alogging(message, mode="error", is_err=True)
466
+ await self.amit(message, mode="error", is_err=True)
451
467
 
452
468
  async def aexception(self, message: str) -> None: # pragma: no cov
453
469
  """Async write trace log with append mode and logging this message with
@@ -455,36 +471,12 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
455
471
 
456
472
  :param message: (str) A message that want to log.
457
473
  """
458
- await self._alogging(message, mode="exception", is_err=True)
474
+ await self.amit(message, mode="exception", is_err=True)
459
475
 
460
476
 
461
477
  class ConsoleTrace(BaseTrace): # pragma: no cov
462
478
  """Console Trace log model."""
463
479
 
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
480
  def writer(
489
481
  self,
490
482
  message: str,
@@ -537,13 +529,13 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
537
529
  """
538
530
  return prepare_newline(Message.from_str(message).prepare(self.extras))
539
531
 
540
- def _logging(
541
- self, message: str, mode: str, *, is_err: bool = False
542
- ) -> None:
532
+ def emit(self, message: str, mode: str, *, is_err: bool = False) -> None:
543
533
  """Write trace log with append mode and logging this message with any
544
534
  logging level.
545
535
 
546
536
  :param message: (str) A message that want to log.
537
+ :param mode: (str)
538
+ :param is_err: (bool)
547
539
  """
548
540
  msg: str = self.make_message(message)
549
541
 
@@ -554,13 +546,15 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
554
546
 
555
547
  getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
556
548
 
557
- async def _alogging(
549
+ async def amit(
558
550
  self, message: str, mode: str, *, is_err: bool = False
559
551
  ) -> None:
560
552
  """Write trace log with append mode and logging this message with any
561
553
  logging level.
562
554
 
563
555
  :param message: (str) A message that want to log.
556
+ :param mode: (str)
557
+ :param is_err: (bool)
564
558
  """
565
559
  msg: str = self.make_message(message)
566
560
 
@@ -572,7 +566,62 @@ class ConsoleTrace(BaseTrace): # pragma: no cov
572
566
  getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
573
567
 
574
568
 
575
- class FileTrace(ConsoleTrace): # pragma: no cov
569
+ class OutsideTrace(ConsoleTrace, ABC):
570
+ model_config = ConfigDict(arbitrary_types_allowed=True)
571
+
572
+ url: ParseResult = Field(description="An URL for create pointer.")
573
+
574
+ @field_validator(
575
+ "url", mode="before", json_schema_input_type=Union[ParseResult, str]
576
+ )
577
+ def __parse_url(cls, value: Union[ParseResult, str]) -> ParseResult:
578
+ if isinstance(value, str):
579
+ return urlparse(value)
580
+ return value
581
+
582
+ @field_serializer("url")
583
+ def __serialize_url(self, value: ParseResult) -> str:
584
+ return value.geturl()
585
+
586
+ @classmethod
587
+ @abstractmethod
588
+ def find_traces(
589
+ cls,
590
+ path: Optional[Path] = None,
591
+ extras: Optional[DictData] = None,
592
+ ) -> Iterator[TraceData]: # pragma: no cov
593
+ """Return iterator of TraceData models from the target pointer.
594
+
595
+ Args:
596
+ path (:obj:`Path`, optional): A pointer path that want to override.
597
+ extras (:obj:`DictData`, optional): An extras parameter that want to
598
+ override default engine config.
599
+
600
+ Returns:
601
+ Iterator[TracData]: An iterator object that generate a TracData
602
+ model.
603
+ """
604
+ raise NotImplementedError(
605
+ "Trace dataclass should implement `find_traces` class-method."
606
+ )
607
+
608
+ @classmethod
609
+ @abstractmethod
610
+ def find_trace_with_id(
611
+ cls,
612
+ run_id: str,
613
+ force_raise: bool = True,
614
+ *,
615
+ path: Optional[Path] = None,
616
+ extras: Optional[DictData] = None,
617
+ ) -> TraceData:
618
+ raise NotImplementedError(
619
+ "Trace dataclass should implement `find_trace_with_id` "
620
+ "class-method."
621
+ )
622
+
623
+
624
+ class FileTrace(OutsideTrace): # pragma: no cov
576
625
  """File Trace dataclass that write file to the local storage."""
577
626
 
578
627
  @classmethod
@@ -587,7 +636,9 @@ class FileTrace(ConsoleTrace): # pragma: no cov
587
636
  :param extras: An extra parameter that want to override core config.
588
637
  """
589
638
  for file in sorted(
590
- (path or dynamic("trace_path", extras=extras)).glob("./run_id=*"),
639
+ (path or Path(dynamic("trace_url", extras=extras).path)).glob(
640
+ "./run_id=*"
641
+ ),
591
642
  key=lambda f: f.lstat().st_mtime,
592
643
  ):
593
644
  yield TraceData.from_path(file)
@@ -608,7 +659,7 @@ class FileTrace(ConsoleTrace): # pragma: no cov
608
659
  :param path: (Path)
609
660
  :param extras: An extra parameter that want to override core config.
610
661
  """
611
- base_path: Path = path or dynamic("trace_path", extras=extras)
662
+ base_path: Path = path or Path(dynamic("trace_url", extras=extras).path)
612
663
  file: Path = base_path / f"run_id={run_id}"
613
664
  if file.exists():
614
665
  return TraceData.from_path(file)
@@ -624,10 +675,14 @@ class FileTrace(ConsoleTrace): # pragma: no cov
624
675
  """Pointer of the target path that use to writing trace log or searching
625
676
  trace log.
626
677
 
678
+ This running ID folder that use to keeping trace log data will use
679
+ a parent running ID first. If it does not set, it will use running ID
680
+ instead.
681
+
627
682
  :rtype: Path
628
683
  """
629
684
  log_file: Path = (
630
- dynamic("trace_path", extras=self.extras)
685
+ Path(unquote_plus(self.url.path))
631
686
  / f"run_id={self.parent_run_id or self.run_id}"
632
687
  )
633
688
  if not log_file.exists():
@@ -710,17 +765,20 @@ class FileTrace(ConsoleTrace): # pragma: no cov
710
765
  await f.write(trace_meta.model_dump_json() + "\n")
711
766
 
712
767
 
713
- class SQLiteTrace(ConsoleTrace): # pragma: no cov
768
+ class SQLiteTrace(OutsideTrace): # pragma: no cov
714
769
  """SQLite Trace dataclass that write trace log to the SQLite database file."""
715
770
 
716
771
  table_name: ClassVar[str] = "audits"
717
772
  schemas: ClassVar[
718
773
  str
719
774
  ] = """
720
- run_id int,
721
- stdout str,
722
- stderr str,
723
- update datetime
775
+ run_id str
776
+ , parent_run_id str
777
+ , type str
778
+ , text str
779
+ , metadata JSON
780
+ , created_at datetime
781
+ , updated_at datetime
724
782
  primary key ( run_id )
725
783
  """
726
784
 
@@ -729,7 +787,8 @@ class SQLiteTrace(ConsoleTrace): # pragma: no cov
729
787
  cls,
730
788
  path: Optional[Path] = None,
731
789
  extras: Optional[DictData] = None,
732
- ) -> Iterator[TraceData]: ...
790
+ ) -> Iterator[TraceData]:
791
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
733
792
 
734
793
  @classmethod
735
794
  def find_trace_with_id(
@@ -739,30 +798,33 @@ class SQLiteTrace(ConsoleTrace): # pragma: no cov
739
798
  *,
740
799
  path: Optional[Path] = None,
741
800
  extras: Optional[DictData] = None,
742
- ) -> TraceData: ...
801
+ ) -> TraceData:
802
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
743
803
 
744
- def make_message(self, message: str) -> str: ...
804
+ def make_message(self, message: str) -> str:
805
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
745
806
 
746
807
  def writer(
747
808
  self,
748
809
  message: str,
749
810
  level: str,
750
811
  is_err: bool = False,
751
- ) -> None: ...
812
+ ) -> None:
813
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
752
814
 
753
815
  def awriter(
754
816
  self,
755
817
  message: str,
756
818
  level: str,
757
819
  is_err: bool = False,
758
- ) -> None: ...
820
+ ) -> None:
821
+ raise NotImplementedError("SQLiteTrace does not implement yet.")
759
822
 
760
823
 
761
- Trace = TypeVar("Trace", bound=BaseTrace)
762
- TraceModel = Union[
763
- ConsoleTrace,
824
+ Trace = Union[
764
825
  FileTrace,
765
826
  SQLiteTrace,
827
+ OutsideTrace,
766
828
  ]
767
829
 
768
830
 
@@ -771,7 +833,7 @@ def get_trace(
771
833
  *,
772
834
  parent_run_id: Optional[str] = None,
773
835
  extras: Optional[DictData] = None,
774
- ) -> TraceModel: # pragma: no cov
836
+ ) -> Trace: # pragma: no cov
775
837
  """Get dynamic Trace instance from the core config (it can override by an
776
838
  extras argument) that passing running ID and parent running ID.
777
839
 
@@ -780,12 +842,31 @@ def get_trace(
780
842
  :param extras: (DictData) An extra parameter that want to override the core
781
843
  config values.
782
844
 
783
- :rtype: TraceLog
845
+ :rtype: Trace
784
846
  """
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 {})
847
+ # NOTE: Allow you to override trace model by the extra parameter.
848
+ map_trace_models: dict[str, type[Trace]] = extras.get(
849
+ "trace_model_mapping", {}
850
+ )
851
+ url: ParseResult
852
+ if (url := dynamic("trace_url", extras=extras)).scheme and (
853
+ url.scheme == "sqlite"
854
+ or (url.scheme == "file" and Path(url.path).is_file())
855
+ ):
856
+ return map_trace_models.get("sqlite", SQLiteTrace)(
857
+ url=url,
858
+ run_id=run_id,
859
+ parent_run_id=parent_run_id,
860
+ extras=(extras or {}),
861
+ )
862
+ elif url.scheme and url.scheme != "file":
863
+ raise NotImplementedError(
864
+ f"Does not implement the outside trace model support for URL: {url}"
788
865
  )
789
- return FileTrace(
790
- run_id=run_id, parent_run_id=parent_run_id, extras=(extras or {})
866
+
867
+ return map_trace_models.get("file", FileTrace)(
868
+ url=url,
869
+ run_id=run_id,
870
+ parent_run_id=parent_run_id,
871
+ extras=(extras or {}),
791
872
  )