ddeutil-workflow 0.0.67__py3-none-any.whl → 0.0.69__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 +14 -12
- ddeutil/workflow/api/__init__.py +20 -19
- ddeutil/workflow/api/log_conf.py +59 -0
- ddeutil/workflow/api/routes/__init__.py +3 -3
- ddeutil/workflow/api/routes/job.py +42 -16
- ddeutil/workflow/api/routes/logs.py +8 -8
- ddeutil/workflow/api/routes/workflows.py +12 -11
- ddeutil/workflow/audits.py +374 -0
- ddeutil/workflow/cli.py +80 -15
- ddeutil/workflow/conf.py +9 -52
- ddeutil/workflow/event.py +6 -5
- ddeutil/workflow/result.py +10 -1
- ddeutil/workflow/stages.py +38 -9
- ddeutil/workflow/{logs.py → traces.py} +168 -387
- ddeutil/workflow/utils.py +1 -52
- ddeutil/workflow/workflow.py +8 -16
- {ddeutil_workflow-0.0.67.dist-info → ddeutil_workflow-0.0.69.dist-info}/METADATA +31 -29
- ddeutil_workflow-0.0.69.dist-info/RECORD +30 -0
- {ddeutil_workflow-0.0.67.dist-info → ddeutil_workflow-0.0.69.dist-info}/WHEEL +1 -1
- ddeutil/workflow/api/logs.py +0 -59
- ddeutil_workflow-0.0.67.dist-info/RECORD +0 -29
- {ddeutil_workflow-0.0.67.dist-info → ddeutil_workflow-0.0.69.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.67.dist-info → ddeutil_workflow-0.0.69.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.67.dist-info → ddeutil_workflow-0.0.69.dist-info}/top_level.txt +0 -0
@@ -3,12 +3,7 @@
|
|
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 `
|
7
|
-
"""A Logs module contain Trace and Audit Pydantic models for process log from
|
8
|
-
the core workflow engine. I separate part of log to 2 types:
|
9
|
-
- Trace: A stdout and stderr log
|
10
|
-
- Audit: An audit release log for tracking incremental running workflow.
|
11
|
-
"""
|
6
|
+
# [x] Use fix config for `set_logging`, and Model initialize step.
|
12
7
|
from __future__ import annotations
|
13
8
|
|
14
9
|
import json
|
@@ -17,7 +12,6 @@ import os
|
|
17
12
|
import re
|
18
13
|
from abc import ABC, abstractmethod
|
19
14
|
from collections.abc import Iterator
|
20
|
-
from datetime import datetime
|
21
15
|
from functools import lru_cache
|
22
16
|
from inspect import Traceback, currentframe, getframeinfo
|
23
17
|
from pathlib import Path
|
@@ -26,7 +20,6 @@ from types import FrameType
|
|
26
20
|
from typing import ClassVar, Final, Literal, Optional, TypeVar, Union
|
27
21
|
|
28
22
|
from pydantic import BaseModel, ConfigDict, Field
|
29
|
-
from pydantic.functional_validators import model_validator
|
30
23
|
from typing_extensions import Self
|
31
24
|
|
32
25
|
from .__types import DictData
|
@@ -34,44 +27,35 @@ from .conf import config, dynamic
|
|
34
27
|
from .utils import cut_id, get_dt_now, prepare_newline
|
35
28
|
|
36
29
|
METADATA: str = "metadata.json"
|
30
|
+
logger = logging.getLogger("ddeutil.workflow")
|
37
31
|
|
38
32
|
|
39
33
|
@lru_cache
|
40
|
-
def
|
41
|
-
"""Return logger object with an input module name
|
34
|
+
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.
|
42
37
|
|
43
38
|
:param name: (str) A module name that want to log.
|
39
|
+
|
40
|
+
:rtype: logging.Logger
|
44
41
|
"""
|
45
|
-
|
42
|
+
_logger = logging.getLogger(name)
|
46
43
|
|
47
44
|
# NOTE: Developers using this package can then disable all logging just for
|
48
45
|
# this package by;
|
49
46
|
#
|
50
47
|
# `logging.getLogger('ddeutil.workflow').propagate = False`
|
51
48
|
#
|
52
|
-
|
49
|
+
_logger.addHandler(logging.NullHandler())
|
53
50
|
|
54
51
|
formatter = logging.Formatter(
|
55
|
-
fmt=config.log_format,
|
56
|
-
datefmt=config.log_datetime_format,
|
52
|
+
fmt=config.log_format, datefmt=config.log_datetime_format
|
57
53
|
)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
return lg
|
64
|
-
|
65
|
-
|
66
|
-
logger = get_logger("ddeutil.workflow")
|
67
|
-
|
68
|
-
|
69
|
-
def get_dt_tznow() -> datetime: # pragma: no cov
|
70
|
-
"""Return the current datetime object that passing the config timezone.
|
71
|
-
|
72
|
-
:rtype: datetime
|
73
|
-
"""
|
74
|
-
return get_dt_now(tz=config.tz)
|
54
|
+
stream_handler = logging.StreamHandler()
|
55
|
+
stream_handler.setFormatter(formatter)
|
56
|
+
_logger.addHandler(stream_handler)
|
57
|
+
_logger.setLevel(logging.DEBUG if config.debug else logging.INFO)
|
58
|
+
return _logger
|
75
59
|
|
76
60
|
|
77
61
|
PREFIX_LOGS: Final[dict[str, dict]] = {
|
@@ -97,8 +81,22 @@ class PrefixMsg(BaseModel):
|
|
97
81
|
from logging message.
|
98
82
|
"""
|
99
83
|
|
100
|
-
name: Optional[str] = Field(default=None)
|
101
|
-
message: Optional[str] = Field(default=None)
|
84
|
+
name: Optional[str] = Field(default=None, description="A prefix name.")
|
85
|
+
message: Optional[str] = Field(default=None, description="A message.")
|
86
|
+
|
87
|
+
@classmethod
|
88
|
+
def from_str(cls, msg: str) -> Self:
|
89
|
+
"""Extract message prefix from an input message.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
msg (str): A message that want to extract.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
PrefixMsg: the validated model from a string message.
|
96
|
+
"""
|
97
|
+
return PrefixMsg.model_validate(
|
98
|
+
obj=PREFIX_LOGS_REGEX.search(msg).groupdict()
|
99
|
+
)
|
102
100
|
|
103
101
|
def prepare(self, extras: Optional[DictData] = None) -> str:
|
104
102
|
"""Prepare message with force add prefix before writing trace log.
|
@@ -117,18 +115,6 @@ class PrefixMsg(BaseModel):
|
|
117
115
|
return f"{emoji}[{name}]: {self.message}"
|
118
116
|
|
119
117
|
|
120
|
-
def extract_msg_prefix(msg: str) -> PrefixMsg:
|
121
|
-
"""Extract message prefix from an input message.
|
122
|
-
|
123
|
-
:param msg: A message that want to extract.
|
124
|
-
|
125
|
-
:rtype: PrefixMsg
|
126
|
-
"""
|
127
|
-
return PrefixMsg.model_validate(
|
128
|
-
obj=PREFIX_LOGS_REGEX.search(msg).groupdict()
|
129
|
-
)
|
130
|
-
|
131
|
-
|
132
118
|
class TraceMeta(BaseModel): # pragma: no cov
|
133
119
|
"""Trace Metadata model for making the current metadata of this CPU, Memory
|
134
120
|
process, and thread data.
|
@@ -247,10 +233,6 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
247
233
|
|
248
234
|
model_config = ConfigDict(frozen=True)
|
249
235
|
|
250
|
-
run_id: str = Field(default="A running ID")
|
251
|
-
parent_run_id: Optional[str] = Field(
|
252
|
-
default=None, description="A parent running ID"
|
253
|
-
)
|
254
236
|
extras: DictData = Field(
|
255
237
|
default_factory=dict,
|
256
238
|
description=(
|
@@ -258,6 +240,11 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
258
240
|
"values."
|
259
241
|
),
|
260
242
|
)
|
243
|
+
run_id: str = Field(description="A running ID")
|
244
|
+
parent_run_id: Optional[str] = Field(
|
245
|
+
default=None,
|
246
|
+
description="A parent running ID",
|
247
|
+
)
|
261
248
|
|
262
249
|
@classmethod
|
263
250
|
@abstractmethod
|
@@ -266,6 +253,17 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
266
253
|
path: Optional[Path] = None,
|
267
254
|
extras: Optional[DictData] = None,
|
268
255
|
) -> Iterator[TraceData]: # pragma: no cov
|
256
|
+
"""Return iterator of TraceData models from the target pointer.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
path (:obj:`Path`, optional): A pointer path that want to override.
|
260
|
+
extras (:obj:`DictData`, optional): An extras parameter that want to
|
261
|
+
override default engine config.
|
262
|
+
|
263
|
+
Returns:
|
264
|
+
Iterator[TracData]: An iterator object that generate a TracData
|
265
|
+
model.
|
266
|
+
"""
|
269
267
|
raise NotImplementedError(
|
270
268
|
"Trace dataclass should implement `find_traces` class-method."
|
271
269
|
)
|
@@ -286,7 +284,12 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
286
284
|
)
|
287
285
|
|
288
286
|
@abstractmethod
|
289
|
-
def writer(
|
287
|
+
def writer(
|
288
|
+
self,
|
289
|
+
message: str,
|
290
|
+
level: str,
|
291
|
+
is_err: bool = False,
|
292
|
+
) -> None:
|
290
293
|
"""Write a trace message after making to target pointer object. The
|
291
294
|
target can be anything be inherited this class and overwrite this method
|
292
295
|
such as file, console, or database.
|
@@ -302,7 +305,10 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
302
305
|
|
303
306
|
@abstractmethod
|
304
307
|
async def awriter(
|
305
|
-
self,
|
308
|
+
self,
|
309
|
+
message: str,
|
310
|
+
level: str,
|
311
|
+
is_err: bool = False,
|
306
312
|
) -> None:
|
307
313
|
"""Async Write a trace message after making to target pointer object.
|
308
314
|
|
@@ -327,6 +333,89 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
327
333
|
"Adjust make message method for this trace object before using."
|
328
334
|
)
|
329
335
|
|
336
|
+
|
337
|
+
class ConsoleTrace(BaseTrace): # pragma: no cov
|
338
|
+
"""Console Trace log model."""
|
339
|
+
|
340
|
+
@classmethod
|
341
|
+
def find_traces(
|
342
|
+
cls,
|
343
|
+
path: Optional[Path] = None,
|
344
|
+
extras: Optional[DictData] = None,
|
345
|
+
) -> Iterator[TraceData]: # pragma: no cov
|
346
|
+
raise NotImplementedError(
|
347
|
+
"Console Trace does not support to find history traces data."
|
348
|
+
)
|
349
|
+
|
350
|
+
@classmethod
|
351
|
+
def find_trace_with_id(
|
352
|
+
cls,
|
353
|
+
run_id: str,
|
354
|
+
force_raise: bool = True,
|
355
|
+
*,
|
356
|
+
path: Optional[Path] = None,
|
357
|
+
extras: Optional[DictData] = None,
|
358
|
+
) -> TraceData:
|
359
|
+
raise NotImplementedError(
|
360
|
+
"Console Trace does not support to find history traces data with "
|
361
|
+
"the specific running ID."
|
362
|
+
)
|
363
|
+
|
364
|
+
def writer(
|
365
|
+
self,
|
366
|
+
message: str,
|
367
|
+
level: str,
|
368
|
+
is_err: bool = False,
|
369
|
+
) -> None:
|
370
|
+
"""Write a trace message after making to target pointer object. The
|
371
|
+
target can be anything be inherited this class and overwrite this method
|
372
|
+
such as file, console, or database.
|
373
|
+
|
374
|
+
:param message: (str) A message after making.
|
375
|
+
:param level: (str) A log level.
|
376
|
+
:param is_err: (bool) A flag for writing with an error trace or not.
|
377
|
+
(Default be False)
|
378
|
+
"""
|
379
|
+
|
380
|
+
async def awriter(
|
381
|
+
self,
|
382
|
+
message: str,
|
383
|
+
level: str,
|
384
|
+
is_err: bool = False,
|
385
|
+
) -> None:
|
386
|
+
"""Async Write a trace message after making to target pointer object.
|
387
|
+
|
388
|
+
:param message: (str) A message after making.
|
389
|
+
:param level: (str) A log level.
|
390
|
+
:param is_err: (bool) A flag for writing with an error trace or not.
|
391
|
+
(Default be False)
|
392
|
+
"""
|
393
|
+
|
394
|
+
@property
|
395
|
+
def cut_id(self) -> str:
|
396
|
+
"""Combine cutting ID of parent running ID if it set.
|
397
|
+
|
398
|
+
:rtype: str
|
399
|
+
"""
|
400
|
+
cut_run_id: str = cut_id(self.run_id)
|
401
|
+
if not self.parent_run_id:
|
402
|
+
return f"{cut_run_id}"
|
403
|
+
|
404
|
+
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
405
|
+
return f"{cut_parent_run_id} -> {cut_run_id}"
|
406
|
+
|
407
|
+
def make_message(self, message: str) -> str:
|
408
|
+
"""Prepare and Make a message before write and log steps.
|
409
|
+
|
410
|
+
:param message: (str) A message that want to prepare and make before.
|
411
|
+
|
412
|
+
:rtype: str
|
413
|
+
"""
|
414
|
+
return prepare_newline(
|
415
|
+
f"({self.cut_id}) "
|
416
|
+
f"{PrefixMsg.from_str(message).prepare(self.extras)}"
|
417
|
+
)
|
418
|
+
|
330
419
|
def __logging(
|
331
420
|
self, message: str, mode: str, *, is_err: bool = False
|
332
421
|
) -> None:
|
@@ -342,7 +431,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
342
431
|
):
|
343
432
|
self.writer(msg, level=mode, is_err=is_err)
|
344
433
|
|
345
|
-
getattr(logger, mode)(msg, stacklevel=3)
|
434
|
+
getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
|
346
435
|
|
347
436
|
def debug(self, message: str):
|
348
437
|
"""Write trace log with append mode and logging this message with the
|
@@ -399,7 +488,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
399
488
|
):
|
400
489
|
await self.awriter(msg, level=mode, is_err=is_err)
|
401
490
|
|
402
|
-
getattr(logger, mode)(msg, stacklevel=3)
|
491
|
+
getattr(logger, mode)(msg, stacklevel=3, extra={"cut_id": self.cut_id})
|
403
492
|
|
404
493
|
async def adebug(self, message: str) -> None: # pragma: no cov
|
405
494
|
"""Async write trace log with append mode and logging this message with
|
@@ -442,7 +531,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
442
531
|
await self.__alogging(message, mode="exception", is_err=True)
|
443
532
|
|
444
533
|
|
445
|
-
class FileTrace(
|
534
|
+
class FileTrace(ConsoleTrace): # pragma: no cov
|
446
535
|
"""File Trace dataclass that write file to the local storage."""
|
447
536
|
|
448
537
|
@classmethod
|
@@ -491,6 +580,11 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
491
580
|
|
492
581
|
@property
|
493
582
|
def pointer(self) -> Path:
|
583
|
+
"""Pointer of the target path that use to writing trace log or searching
|
584
|
+
trace log.
|
585
|
+
|
586
|
+
:rtype: Path
|
587
|
+
"""
|
494
588
|
log_file: Path = (
|
495
589
|
dynamic("trace_path", extras=self.extras)
|
496
590
|
/ f"run_id={self.parent_run_id or self.run_id}"
|
@@ -499,31 +593,12 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
499
593
|
log_file.mkdir(parents=True)
|
500
594
|
return log_file
|
501
595
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
:
|
507
|
-
|
508
|
-
cut_run_id: str = cut_id(self.run_id)
|
509
|
-
if not self.parent_run_id:
|
510
|
-
return f"{cut_run_id}"
|
511
|
-
|
512
|
-
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
513
|
-
return f"{cut_parent_run_id} -> {cut_run_id}"
|
514
|
-
|
515
|
-
def make_message(self, message: str) -> str:
|
516
|
-
"""Prepare and Make a message before write and log steps.
|
517
|
-
|
518
|
-
:param message: (str) A message that want to prepare and make before.
|
519
|
-
|
520
|
-
:rtype: str
|
521
|
-
"""
|
522
|
-
return prepare_newline(
|
523
|
-
f"({self.cut_id}) {extract_msg_prefix(message).prepare(self.extras)}"
|
524
|
-
)
|
525
|
-
|
526
|
-
def writer(self, message: str, level: str, is_err: bool = False) -> None:
|
596
|
+
def writer(
|
597
|
+
self,
|
598
|
+
message: str,
|
599
|
+
level: str,
|
600
|
+
is_err: bool = False,
|
601
|
+
) -> None:
|
527
602
|
"""Write a trace message after making to target file and write metadata
|
528
603
|
in the same path of standard files.
|
529
604
|
|
@@ -555,7 +630,10 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
555
630
|
f.write(trace_meta.model_dump_json() + "\n")
|
556
631
|
|
557
632
|
async def awriter(
|
558
|
-
self,
|
633
|
+
self,
|
634
|
+
message: str,
|
635
|
+
level: str,
|
636
|
+
is_err: bool = False,
|
559
637
|
) -> None: # pragma: no cov
|
560
638
|
"""Write with async mode."""
|
561
639
|
if not dynamic("enable_write_log", extras=self.extras):
|
@@ -583,7 +661,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
583
661
|
await f.write(trace_meta.model_dump_json() + "\n")
|
584
662
|
|
585
663
|
|
586
|
-
class SQLiteTrace(
|
664
|
+
class SQLiteTrace(ConsoleTrace): # pragma: no cov
|
587
665
|
"""SQLite Trace dataclass that write trace log to the SQLite database file."""
|
588
666
|
|
589
667
|
table_name: ClassVar[str] = "audits"
|
@@ -617,16 +695,23 @@ class SQLiteTrace(BaseTrace): # pragma: no cov
|
|
617
695
|
def make_message(self, message: str) -> str: ...
|
618
696
|
|
619
697
|
def writer(
|
620
|
-
self,
|
698
|
+
self,
|
699
|
+
message: str,
|
700
|
+
level: str,
|
701
|
+
is_err: bool = False,
|
621
702
|
) -> None: ...
|
622
703
|
|
623
704
|
def awriter(
|
624
|
-
self,
|
705
|
+
self,
|
706
|
+
message: str,
|
707
|
+
level: str,
|
708
|
+
is_err: bool = False,
|
625
709
|
) -> None: ...
|
626
710
|
|
627
711
|
|
628
712
|
Trace = TypeVar("Trace", bound=BaseTrace)
|
629
713
|
TraceModel = Union[
|
714
|
+
ConsoleTrace,
|
630
715
|
FileTrace,
|
631
716
|
SQLiteTrace,
|
632
717
|
]
|
@@ -655,307 +740,3 @@ def get_trace(
|
|
655
740
|
return FileTrace(
|
656
741
|
run_id=run_id, parent_run_id=parent_run_id, extras=(extras or {})
|
657
742
|
)
|
658
|
-
|
659
|
-
|
660
|
-
class BaseAudit(BaseModel, ABC):
|
661
|
-
"""Base Audit Pydantic Model with abstraction class property that implement
|
662
|
-
only model fields. This model should to use with inherit to logging
|
663
|
-
subclass like file, sqlite, etc.
|
664
|
-
"""
|
665
|
-
|
666
|
-
extras: DictData = Field(
|
667
|
-
default_factory=dict,
|
668
|
-
description="An extras parameter that want to override core config",
|
669
|
-
)
|
670
|
-
name: str = Field(description="A workflow name.")
|
671
|
-
release: datetime = Field(description="A release datetime.")
|
672
|
-
type: str = Field(description="A running type before logging.")
|
673
|
-
context: DictData = Field(
|
674
|
-
default_factory=dict,
|
675
|
-
description="A context that receive from a workflow execution result.",
|
676
|
-
)
|
677
|
-
parent_run_id: Optional[str] = Field(
|
678
|
-
default=None, description="A parent running ID."
|
679
|
-
)
|
680
|
-
run_id: str = Field(description="A running ID")
|
681
|
-
update: datetime = Field(default_factory=get_dt_tznow)
|
682
|
-
execution_time: float = Field(default=0, description="An execution time.")
|
683
|
-
|
684
|
-
@model_validator(mode="after")
|
685
|
-
def __model_action(self) -> Self:
|
686
|
-
"""Do before the Audit action with WORKFLOW_AUDIT_ENABLE_WRITE env variable.
|
687
|
-
|
688
|
-
:rtype: Self
|
689
|
-
"""
|
690
|
-
if dynamic("enable_write_audit", extras=self.extras):
|
691
|
-
self.do_before()
|
692
|
-
return self
|
693
|
-
|
694
|
-
@classmethod
|
695
|
-
@abstractmethod
|
696
|
-
def is_pointed(
|
697
|
-
cls,
|
698
|
-
name: str,
|
699
|
-
release: datetime,
|
700
|
-
*,
|
701
|
-
extras: Optional[DictData] = None,
|
702
|
-
) -> bool:
|
703
|
-
raise NotImplementedError(
|
704
|
-
"Audit should implement `is_pointed` class-method"
|
705
|
-
)
|
706
|
-
|
707
|
-
@classmethod
|
708
|
-
@abstractmethod
|
709
|
-
def find_audits(
|
710
|
-
cls, name: str, *, extras: Optional[DictData] = None
|
711
|
-
) -> Iterator[Self]:
|
712
|
-
raise NotImplementedError(
|
713
|
-
"Audit should implement `find_audits` class-method"
|
714
|
-
)
|
715
|
-
|
716
|
-
@classmethod
|
717
|
-
@abstractmethod
|
718
|
-
def find_audit_with_release(
|
719
|
-
cls,
|
720
|
-
name: str,
|
721
|
-
release: Optional[datetime] = None,
|
722
|
-
*,
|
723
|
-
extras: Optional[DictData] = None,
|
724
|
-
) -> Self:
|
725
|
-
raise NotImplementedError(
|
726
|
-
"Audit should implement `find_audit_with_release` class-method"
|
727
|
-
)
|
728
|
-
|
729
|
-
def do_before(self) -> None: # pragma: no cov
|
730
|
-
"""To something before end up of initial log model."""
|
731
|
-
|
732
|
-
@abstractmethod
|
733
|
-
def save(self, excluded: Optional[list[str]]) -> None: # pragma: no cov
|
734
|
-
"""Save this model logging to target logging store."""
|
735
|
-
raise NotImplementedError("Audit should implement ``save`` method.")
|
736
|
-
|
737
|
-
|
738
|
-
class FileAudit(BaseAudit):
|
739
|
-
"""File Audit Pydantic Model that use to saving log data from result of
|
740
|
-
workflow execution. It inherits from BaseAudit model that implement the
|
741
|
-
``self.save`` method for file.
|
742
|
-
"""
|
743
|
-
|
744
|
-
filename_fmt: ClassVar[str] = (
|
745
|
-
"workflow={name}/release={release:%Y%m%d%H%M%S}"
|
746
|
-
)
|
747
|
-
|
748
|
-
def do_before(self) -> None:
|
749
|
-
"""Create directory of release before saving log file."""
|
750
|
-
self.pointer().mkdir(parents=True, exist_ok=True)
|
751
|
-
|
752
|
-
@classmethod
|
753
|
-
def find_audits(
|
754
|
-
cls, name: str, *, extras: Optional[DictData] = None
|
755
|
-
) -> Iterator[Self]:
|
756
|
-
"""Generate the audit data that found from logs path with specific a
|
757
|
-
workflow name.
|
758
|
-
|
759
|
-
:param name: A workflow name that want to search release logging data.
|
760
|
-
:param extras: An extra parameter that want to override core config.
|
761
|
-
|
762
|
-
:rtype: Iterator[Self]
|
763
|
-
"""
|
764
|
-
pointer: Path = (
|
765
|
-
dynamic("audit_path", extras=extras) / f"workflow={name}"
|
766
|
-
)
|
767
|
-
if not pointer.exists():
|
768
|
-
raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
|
769
|
-
|
770
|
-
for file in pointer.glob("./release=*/*.log"):
|
771
|
-
with file.open(mode="r", encoding="utf-8") as f:
|
772
|
-
yield cls.model_validate(obj=json.load(f))
|
773
|
-
|
774
|
-
@classmethod
|
775
|
-
def find_audit_with_release(
|
776
|
-
cls,
|
777
|
-
name: str,
|
778
|
-
release: Optional[datetime] = None,
|
779
|
-
*,
|
780
|
-
extras: Optional[DictData] = None,
|
781
|
-
) -> Self:
|
782
|
-
"""Return the audit data that found from logs path with specific
|
783
|
-
workflow name and release values. If a release does not pass to an input
|
784
|
-
argument, it will return the latest release from the current log path.
|
785
|
-
|
786
|
-
:param name: (str) A workflow name that want to search log.
|
787
|
-
:param release: (datetime) A release datetime that want to search log.
|
788
|
-
:param extras: An extra parameter that want to override core config.
|
789
|
-
|
790
|
-
:raise FileNotFoundError:
|
791
|
-
:raise NotImplementedError: If an input release does not pass to this
|
792
|
-
method. Because this method does not implement latest log.
|
793
|
-
|
794
|
-
:rtype: Self
|
795
|
-
"""
|
796
|
-
if release is None:
|
797
|
-
raise NotImplementedError("Find latest log does not implement yet.")
|
798
|
-
|
799
|
-
pointer: Path = (
|
800
|
-
dynamic("audit_path", extras=extras)
|
801
|
-
/ f"workflow={name}/release={release:%Y%m%d%H%M%S}"
|
802
|
-
)
|
803
|
-
if not pointer.exists():
|
804
|
-
raise FileNotFoundError(
|
805
|
-
f"Pointer: ./logs/workflow={name}/"
|
806
|
-
f"release={release:%Y%m%d%H%M%S} does not found."
|
807
|
-
)
|
808
|
-
|
809
|
-
latest_file: Path = max(pointer.glob("./*.log"), key=os.path.getctime)
|
810
|
-
with latest_file.open(mode="r", encoding="utf-8") as f:
|
811
|
-
return cls.model_validate(obj=json.load(f))
|
812
|
-
|
813
|
-
@classmethod
|
814
|
-
def is_pointed(
|
815
|
-
cls,
|
816
|
-
name: str,
|
817
|
-
release: datetime,
|
818
|
-
*,
|
819
|
-
extras: Optional[DictData] = None,
|
820
|
-
) -> bool:
|
821
|
-
"""Check the release log already pointed or created at the destination
|
822
|
-
log path.
|
823
|
-
|
824
|
-
:param name: (str) A workflow name.
|
825
|
-
:param release: (datetime) A release datetime.
|
826
|
-
:param extras: An extra parameter that want to override core config.
|
827
|
-
|
828
|
-
:rtype: bool
|
829
|
-
:return: Return False if the release log was not pointed or created.
|
830
|
-
"""
|
831
|
-
# NOTE: Return False if enable writing log flag does not set.
|
832
|
-
if not dynamic("enable_write_audit", extras=extras):
|
833
|
-
return False
|
834
|
-
|
835
|
-
# NOTE: create pointer path that use the same logic of pointer method.
|
836
|
-
pointer: Path = dynamic(
|
837
|
-
"audit_path", extras=extras
|
838
|
-
) / cls.filename_fmt.format(name=name, release=release)
|
839
|
-
|
840
|
-
return pointer.exists()
|
841
|
-
|
842
|
-
def pointer(self) -> Path:
|
843
|
-
"""Return release directory path that was generated from model data.
|
844
|
-
|
845
|
-
:rtype: Path
|
846
|
-
"""
|
847
|
-
return dynamic(
|
848
|
-
"audit_path", extras=self.extras
|
849
|
-
) / self.filename_fmt.format(name=self.name, release=self.release)
|
850
|
-
|
851
|
-
def save(self, excluded: Optional[list[str]] = None) -> Self:
|
852
|
-
"""Save logging data that receive a context data from a workflow
|
853
|
-
execution result.
|
854
|
-
|
855
|
-
:param excluded: An excluded list of key name that want to pass in the
|
856
|
-
model_dump method.
|
857
|
-
|
858
|
-
:rtype: Self
|
859
|
-
"""
|
860
|
-
trace: TraceModel = get_trace(
|
861
|
-
self.run_id,
|
862
|
-
parent_run_id=self.parent_run_id,
|
863
|
-
extras=self.extras,
|
864
|
-
)
|
865
|
-
|
866
|
-
# NOTE: Check environ variable was set for real writing.
|
867
|
-
if not dynamic("enable_write_audit", extras=self.extras):
|
868
|
-
trace.debug("[AUDIT]: Skip writing log cause config was set")
|
869
|
-
return self
|
870
|
-
|
871
|
-
log_file: Path = (
|
872
|
-
self.pointer() / f"{self.parent_run_id or self.run_id}.log"
|
873
|
-
)
|
874
|
-
log_file.write_text(
|
875
|
-
json.dumps(
|
876
|
-
self.model_dump(exclude=excluded),
|
877
|
-
default=str,
|
878
|
-
indent=2,
|
879
|
-
),
|
880
|
-
encoding="utf-8",
|
881
|
-
)
|
882
|
-
return self
|
883
|
-
|
884
|
-
|
885
|
-
class SQLiteAudit(BaseAudit): # pragma: no cov
|
886
|
-
"""SQLite Audit Pydantic Model."""
|
887
|
-
|
888
|
-
table_name: ClassVar[str] = "audits"
|
889
|
-
schemas: ClassVar[
|
890
|
-
str
|
891
|
-
] = """
|
892
|
-
workflow str,
|
893
|
-
release int,
|
894
|
-
type str,
|
895
|
-
context json,
|
896
|
-
parent_run_id int,
|
897
|
-
run_id int,
|
898
|
-
update datetime
|
899
|
-
primary key ( run_id )
|
900
|
-
"""
|
901
|
-
|
902
|
-
@classmethod
|
903
|
-
def is_pointed(
|
904
|
-
cls,
|
905
|
-
name: str,
|
906
|
-
release: datetime,
|
907
|
-
*,
|
908
|
-
extras: Optional[DictData] = None,
|
909
|
-
) -> bool: ...
|
910
|
-
|
911
|
-
@classmethod
|
912
|
-
def find_audits(
|
913
|
-
cls, name: str, *, extras: Optional[DictData] = None
|
914
|
-
) -> Iterator[Self]: ...
|
915
|
-
|
916
|
-
@classmethod
|
917
|
-
def find_audit_with_release(
|
918
|
-
cls,
|
919
|
-
name: str,
|
920
|
-
release: Optional[datetime] = None,
|
921
|
-
*,
|
922
|
-
extras: Optional[DictData] = None,
|
923
|
-
) -> Self: ...
|
924
|
-
|
925
|
-
def save(self, excluded: Optional[list[str]]) -> SQLiteAudit:
|
926
|
-
"""Save logging data that receive a context data from a workflow
|
927
|
-
execution result.
|
928
|
-
"""
|
929
|
-
trace: TraceModel = get_trace(
|
930
|
-
self.run_id,
|
931
|
-
parent_run_id=self.parent_run_id,
|
932
|
-
extras=self.extras,
|
933
|
-
)
|
934
|
-
|
935
|
-
# NOTE: Check environ variable was set for real writing.
|
936
|
-
if not dynamic("enable_write_audit", extras=self.extras):
|
937
|
-
trace.debug("[AUDIT]: Skip writing log cause config was set")
|
938
|
-
return self
|
939
|
-
|
940
|
-
raise NotImplementedError("SQLiteAudit does not implement yet.")
|
941
|
-
|
942
|
-
|
943
|
-
Audit = TypeVar("Audit", bound=BaseAudit)
|
944
|
-
AuditModel = Union[
|
945
|
-
FileAudit,
|
946
|
-
SQLiteAudit,
|
947
|
-
]
|
948
|
-
|
949
|
-
|
950
|
-
def get_audit(
|
951
|
-
extras: Optional[DictData] = None,
|
952
|
-
) -> type[AuditModel]: # pragma: no cov
|
953
|
-
"""Get an audit class that dynamic base on the config audit path value.
|
954
|
-
|
955
|
-
:param extras: An extra parameter that want to override the core config.
|
956
|
-
|
957
|
-
:rtype: type[Audit]
|
958
|
-
"""
|
959
|
-
if dynamic("audit_path", extras=extras).is_file():
|
960
|
-
return SQLiteAudit
|
961
|
-
return FileAudit
|