ddeutil-workflow 0.0.81__py3-none-any.whl → 0.0.83__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 +2 -1
- ddeutil/workflow/__cron.py +1 -1
- ddeutil/workflow/__init__.py +21 -7
- ddeutil/workflow/__main__.py +280 -1
- ddeutil/workflow/__types.py +10 -1
- ddeutil/workflow/api/routes/job.py +2 -2
- ddeutil/workflow/api/routes/logs.py +8 -61
- ddeutil/workflow/audits.py +101 -49
- ddeutil/workflow/conf.py +45 -25
- ddeutil/workflow/errors.py +12 -0
- ddeutil/workflow/event.py +34 -11
- ddeutil/workflow/job.py +75 -31
- ddeutil/workflow/result.py +73 -22
- ddeutil/workflow/stages.py +625 -375
- ddeutil/workflow/traces.py +71 -27
- ddeutil/workflow/utils.py +41 -24
- ddeutil/workflow/workflow.py +97 -124
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.83.dist-info}/METADATA +1 -1
- ddeutil_workflow-0.0.83.dist-info/RECORD +35 -0
- ddeutil/workflow/cli.py +0 -284
- ddeutil_workflow-0.0.81.dist-info/RECORD +0 -36
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.83.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.83.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.83.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.83.dist-info}/top_level.txt +0 -0
ddeutil/workflow/traces.py
CHANGED
@@ -29,7 +29,16 @@ from inspect import Traceback, currentframe, getframeinfo
|
|
29
29
|
from pathlib import Path
|
30
30
|
from threading import Lock, get_ident
|
31
31
|
from types import FrameType
|
32
|
-
from typing import
|
32
|
+
from typing import (
|
33
|
+
Annotated,
|
34
|
+
Any,
|
35
|
+
ClassVar,
|
36
|
+
Final,
|
37
|
+
Literal,
|
38
|
+
Optional,
|
39
|
+
TypeVar,
|
40
|
+
Union,
|
41
|
+
)
|
33
42
|
from zoneinfo import ZoneInfo
|
34
43
|
|
35
44
|
from pydantic import BaseModel, Field, PrivateAttr
|
@@ -42,6 +51,8 @@ from .utils import cut_id, get_dt_now, prepare_newline
|
|
42
51
|
|
43
52
|
logger = logging.getLogger("ddeutil.workflow")
|
44
53
|
Level = Literal["debug", "info", "warning", "error", "exception"]
|
54
|
+
EMJ_ALERT: str = "🚨"
|
55
|
+
EMJ_SKIP: str = "⏭️"
|
45
56
|
|
46
57
|
|
47
58
|
@lru_cache
|
@@ -86,9 +97,10 @@ PREFIX_LOGS: Final[dict[str, dict]] = {
|
|
86
97
|
"emoji": "⚙️",
|
87
98
|
"desc": "logs from any usage from custom caller function.",
|
88
99
|
},
|
100
|
+
"NESTED": {"emoji": "⛓️", "desc": "logs from stages module."},
|
89
101
|
"STAGE": {"emoji": "🔗", "desc": "logs from stages module."},
|
90
|
-
"JOB": {"emoji": "
|
91
|
-
"WORKFLOW": {"emoji": "
|
102
|
+
"JOB": {"emoji": "🏗", "desc": "logs from job module."},
|
103
|
+
"WORKFLOW": {"emoji": "👟", "desc": "logs from workflow module."},
|
92
104
|
"RELEASE": {"emoji": "📅", "desc": "logs from release workflow method."},
|
93
105
|
"POKING": {"emoji": "⏰", "desc": "logs from poke workflow method."},
|
94
106
|
"AUDIT": {"emoji": "📌", "desc": "logs from audit model."},
|
@@ -229,7 +241,7 @@ class Metadata(BaseModel): # pragma: no cov
|
|
229
241
|
default=None, description="Environment (dev, staging, prod)."
|
230
242
|
)
|
231
243
|
|
232
|
-
# System context
|
244
|
+
# NOTE: System context
|
233
245
|
hostname: Optional[str] = Field(
|
234
246
|
default=None, description="Hostname where workflow is running."
|
235
247
|
)
|
@@ -243,7 +255,7 @@ class Metadata(BaseModel): # pragma: no cov
|
|
243
255
|
default=None, description="Workflow package version."
|
244
256
|
)
|
245
257
|
|
246
|
-
# Custom metadata
|
258
|
+
# NOTE: Custom metadata
|
247
259
|
tags: Optional[list[str]] = Field(
|
248
260
|
default_factory=list, description="Custom tags for categorization."
|
249
261
|
)
|
@@ -310,6 +322,8 @@ class Metadata(BaseModel): # pragma: no cov
|
|
310
322
|
import socket
|
311
323
|
import sys
|
312
324
|
|
325
|
+
from .__about__ import __version__
|
326
|
+
|
313
327
|
frame: Optional[FrameType] = currentframe()
|
314
328
|
if frame is None:
|
315
329
|
raise ValueError("Cannot get current frame")
|
@@ -374,7 +388,7 @@ class Metadata(BaseModel): # pragma: no cov
|
|
374
388
|
hostname=hostname,
|
375
389
|
ip_address=ip_address,
|
376
390
|
python_version=python_version,
|
377
|
-
package_version=
|
391
|
+
package_version=__version__,
|
378
392
|
# NOTE: Custom metadata
|
379
393
|
tags=extras_data.get("tags", []),
|
380
394
|
metadata=extras_data.get("metadata", {}),
|
@@ -427,6 +441,9 @@ class BaseHandler(BaseModel, ABC):
|
|
427
441
|
self, metadata: list[Metadata], *, extra: Optional[DictData] = None
|
428
442
|
) -> None: ...
|
429
443
|
|
444
|
+
def pre(self) -> None:
|
445
|
+
"""Pre-process of handler that will execute when start create trance."""
|
446
|
+
|
430
447
|
|
431
448
|
class ConsoleHandler(BaseHandler):
|
432
449
|
"""Console Handler model."""
|
@@ -460,14 +477,20 @@ class FileHandler(BaseHandler):
|
|
460
477
|
metadata_filename: ClassVar[str] = "metadata.txt"
|
461
478
|
|
462
479
|
type: Literal["file"] = "file"
|
463
|
-
path: str = Field(
|
480
|
+
path: str = Field(
|
481
|
+
description=(
|
482
|
+
"A file path that use to save all trace log files that include "
|
483
|
+
"stdout, stderr, and metadata."
|
484
|
+
)
|
485
|
+
)
|
464
486
|
format: str = Field(
|
465
487
|
default=(
|
466
488
|
"{datetime} ({process:5d}, {thread:5d}) ({cut_id}) {message:120s} "
|
467
489
|
"({filename}:{lineno})"
|
468
|
-
)
|
490
|
+
),
|
491
|
+
description="A trace log format that write on stdout and stderr files.",
|
469
492
|
)
|
470
|
-
buffer_size: int = 8192
|
493
|
+
buffer_size: int = Field(default=8192)
|
471
494
|
|
472
495
|
# NOTE: Private attrs for the internal process.
|
473
496
|
_lock: Lock = PrivateAttr(default_factory=Lock)
|
@@ -488,7 +511,9 @@ class FileHandler(BaseHandler):
|
|
488
511
|
log_file.mkdir(parents=True)
|
489
512
|
return log_file
|
490
513
|
|
491
|
-
def pre(self) -> None:
|
514
|
+
def pre(self) -> None: # pragma: no cov
|
515
|
+
if not (p := Path(self.path)).exists():
|
516
|
+
p.mkdir(parents=True)
|
492
517
|
|
493
518
|
def emit(
|
494
519
|
self,
|
@@ -496,6 +521,7 @@ class FileHandler(BaseHandler):
|
|
496
521
|
*,
|
497
522
|
extra: Optional[DictData] = None,
|
498
523
|
) -> None:
|
524
|
+
"""Emit trace log."""
|
499
525
|
pointer: Path = self.pointer(metadata.pointer_id)
|
500
526
|
std_file = "stderr" if metadata.error_flag else "stdout"
|
501
527
|
with self._lock:
|
@@ -518,7 +544,9 @@ class FileHandler(BaseHandler):
|
|
518
544
|
try:
|
519
545
|
import aiofiles
|
520
546
|
except ImportError as e:
|
521
|
-
raise ImportError(
|
547
|
+
raise ImportError(
|
548
|
+
"Async mode need to install `aiofiles` package first"
|
549
|
+
) from e
|
522
550
|
|
523
551
|
with self._lock:
|
524
552
|
pointer: Path = self.pointer(metadata.pointer_id)
|
@@ -538,6 +566,7 @@ class FileHandler(BaseHandler):
|
|
538
566
|
def flush(
|
539
567
|
self, metadata: list[Metadata], *, extra: Optional[DictData] = None
|
540
568
|
) -> None:
|
569
|
+
"""Flush logs."""
|
541
570
|
with self._lock:
|
542
571
|
pointer: Path = self.pointer(metadata[0].pointer_id)
|
543
572
|
stdout_file = open(
|
@@ -613,7 +642,7 @@ class FileHandler(BaseHandler):
|
|
613
642
|
"""Find trace logs.
|
614
643
|
|
615
644
|
Args:
|
616
|
-
path: A trace path that want to find.
|
645
|
+
path (Path | None, default None): A trace path that want to find.
|
617
646
|
"""
|
618
647
|
for file in sorted(
|
619
648
|
(path or Path(self.path)).glob("./run_id=*"),
|
@@ -634,6 +663,9 @@ class FileHandler(BaseHandler):
|
|
634
663
|
run_id: A running ID of trace log.
|
635
664
|
force_raise: Whether to raise an exception if not found.
|
636
665
|
path: Optional path override.
|
666
|
+
|
667
|
+
Returns:
|
668
|
+
TraceData: A TranceData instance that already passed searching data.
|
637
669
|
"""
|
638
670
|
base_path: Path = path or self.path
|
639
671
|
file: Path = base_path / f"run_id={run_id}"
|
@@ -757,7 +789,8 @@ class SQLiteHandler(BaseHandler): # pragma: no cov
|
|
757
789
|
metadata: Metadata,
|
758
790
|
*,
|
759
791
|
extra: Optional[DictData] = None,
|
760
|
-
) -> None:
|
792
|
+
) -> None:
|
793
|
+
raise NotImplementedError("Does not implement async emit yet.")
|
761
794
|
|
762
795
|
def flush(
|
763
796
|
self, metadata: list[Metadata], *, extra: Optional[DictData] = None
|
@@ -1506,7 +1539,6 @@ class ElasticHandler(BaseHandler): # pragma: no cov
|
|
1506
1539
|
try:
|
1507
1540
|
from elasticsearch import Elasticsearch
|
1508
1541
|
|
1509
|
-
# Create client
|
1510
1542
|
client = Elasticsearch(
|
1511
1543
|
hosts=es_hosts if isinstance(es_hosts, list) else [es_hosts],
|
1512
1544
|
basic_auth=(
|
@@ -1653,8 +1685,6 @@ class ElasticHandler(BaseHandler): # pragma: no cov
|
|
1653
1685
|
|
1654
1686
|
for hit in response["hits"]["hits"]:
|
1655
1687
|
source = hit["_source"]
|
1656
|
-
|
1657
|
-
# Convert to TraceMeta
|
1658
1688
|
trace_meta = Metadata(
|
1659
1689
|
run_id=source["run_id"],
|
1660
1690
|
parent_run_id=source["parent_run_id"],
|
@@ -1724,6 +1754,7 @@ class ElasticHandler(BaseHandler): # pragma: no cov
|
|
1724
1754
|
return TraceData(stdout="", stderr="")
|
1725
1755
|
|
1726
1756
|
|
1757
|
+
Handler = TypeVar("Handler", bound=BaseHandler)
|
1727
1758
|
TraceHandler = Annotated[
|
1728
1759
|
Union[
|
1729
1760
|
ConsoleHandler,
|
@@ -1866,7 +1897,7 @@ class BaseAsyncEmit(ABC):
|
|
1866
1897
|
await self.amit(msg, level="exception")
|
1867
1898
|
|
1868
1899
|
|
1869
|
-
class
|
1900
|
+
class Trace(BaseModel, BaseEmit, BaseAsyncEmit):
|
1870
1901
|
"""Trace Manager model that keep all trance handler and emit log to its
|
1871
1902
|
handler.
|
1872
1903
|
"""
|
@@ -1955,7 +1986,7 @@ class TraceManager(BaseModel, BaseEmit, BaseAsyncEmit):
|
|
1955
1986
|
any logging level.
|
1956
1987
|
|
1957
1988
|
Args:
|
1958
|
-
msg: A message that want to log.
|
1989
|
+
msg (str): A message that want to log.
|
1959
1990
|
level (Level): A logging mode.
|
1960
1991
|
"""
|
1961
1992
|
_msg: str = self.make_message(msg)
|
@@ -2005,10 +2036,12 @@ class TraceManager(BaseModel, BaseEmit, BaseAsyncEmit):
|
|
2005
2036
|
def get_trace(
|
2006
2037
|
run_id: str,
|
2007
2038
|
*,
|
2039
|
+
handlers: list[Union[DictData, Handler]] = None,
|
2008
2040
|
parent_run_id: Optional[str] = None,
|
2009
2041
|
extras: Optional[DictData] = None,
|
2010
|
-
|
2011
|
-
|
2042
|
+
auto_pre_process: bool = False,
|
2043
|
+
) -> Trace:
|
2044
|
+
"""Get dynamic Trace instance from the core config.
|
2012
2045
|
|
2013
2046
|
This factory function returns the appropriate trace implementation based on
|
2014
2047
|
configuration. It can be overridden by extras argument and accepts running ID
|
@@ -2017,16 +2050,27 @@ def get_trace(
|
|
2017
2050
|
Args:
|
2018
2051
|
run_id (str): A running ID.
|
2019
2052
|
parent_run_id (str | None, default None): A parent running ID.
|
2053
|
+
handlers (list):
|
2020
2054
|
extras: An extra parameter that want to override the core
|
2021
2055
|
config values.
|
2056
|
+
auto_pre_process (bool, default False)
|
2022
2057
|
|
2023
2058
|
Returns:
|
2024
|
-
|
2059
|
+
Trace: The appropriate trace instance.
|
2025
2060
|
"""
|
2026
|
-
handlers = dynamic(
|
2027
|
-
|
2028
|
-
|
2029
|
-
|
2030
|
-
|
2031
|
-
|
2061
|
+
handlers: list[DictData] = dynamic(
|
2062
|
+
"trace_handlers", f=handlers, extras=extras
|
2063
|
+
)
|
2064
|
+
trace: Trace = Trace.model_validate(
|
2065
|
+
{
|
2066
|
+
"run_id": run_id,
|
2067
|
+
"parent_run_id": parent_run_id,
|
2068
|
+
"handlers": handlers,
|
2069
|
+
"extras": extras or {},
|
2070
|
+
}
|
2032
2071
|
)
|
2072
|
+
# NOTE: Start pre-process when start create trace.
|
2073
|
+
if auto_pre_process:
|
2074
|
+
for handler in trace.handlers:
|
2075
|
+
handler.pre()
|
2076
|
+
return trace
|
ddeutil/workflow/utils.py
CHANGED
@@ -8,26 +8,6 @@
|
|
8
8
|
This module provides essential utility functions used throughout the workflow
|
9
9
|
system for ID generation, datetime handling, string processing, template
|
10
10
|
operations, and other common tasks.
|
11
|
-
|
12
|
-
Functions:
|
13
|
-
to_train: Convert camel case strings to train case format
|
14
|
-
prepare_newline: Format messages with multiple newlines
|
15
|
-
replace_sec: Replace seconds and microseconds in datetime objects
|
16
|
-
clear_tz: Clear timezone info from datetime objects
|
17
|
-
get_dt_now: Get current datetime with timezone
|
18
|
-
get_d_now: Get current date
|
19
|
-
get_diff_sec: Calculate time difference in seconds
|
20
|
-
reach_next_minute: Check if datetime reaches next minute
|
21
|
-
wait_until_next_minute: Wait until next minute
|
22
|
-
delay: Add random delay to execution
|
23
|
-
gen_id: Generate unique identifiers for workflow components
|
24
|
-
default_gen_id: Generate default running ID
|
25
|
-
make_exec: Make files executable
|
26
|
-
filter_func: Filter function objects from data structures
|
27
|
-
cross_product: Generate cross product of matrix values
|
28
|
-
cut_id: Cut running ID to specified length
|
29
|
-
dump_all: Serialize nested BaseModel objects to dictionaries
|
30
|
-
obj_name: Get object name or class name
|
31
11
|
"""
|
32
12
|
from __future__ import annotations
|
33
13
|
|
@@ -218,7 +198,10 @@ def gen_id(
|
|
218
198
|
hashing value length to 10 if simple mode is enabled.
|
219
199
|
|
220
200
|
Simple Mode Format:
|
221
|
-
|
201
|
+
|
202
|
+
The format of ID include full datetime and hashing identity.
|
203
|
+
|
204
|
+
YYYY MM DD HH MM SS ffffff T **********
|
222
205
|
year month day hour minute second microsecond sep simple-id
|
223
206
|
|
224
207
|
Args:
|
@@ -250,6 +233,33 @@ def gen_id(
|
|
250
233
|
).hexdigest()
|
251
234
|
|
252
235
|
|
236
|
+
def extract_id(
|
237
|
+
name: str,
|
238
|
+
run_id: Optional[str] = None,
|
239
|
+
extras: Optional[DictData] = None,
|
240
|
+
) -> tuple[str, str]:
|
241
|
+
"""Extract the parent ID and running ID. If the `run_id` parameter was
|
242
|
+
passed, it will replace the parent_run_id with this value and re-generate
|
243
|
+
new running ID for it instead.
|
244
|
+
|
245
|
+
Args:
|
246
|
+
name (str): A name for generate hashing value for the `gen_id` function.
|
247
|
+
run_id (str | None, default None):
|
248
|
+
extras:
|
249
|
+
|
250
|
+
Returns:
|
251
|
+
tuple[str, str]: A pair of parent running ID and running ID.
|
252
|
+
"""
|
253
|
+
generated = gen_id(name, unique=True, extras=extras)
|
254
|
+
if run_id:
|
255
|
+
parent_run_id: str = run_id
|
256
|
+
run_id: str = generated
|
257
|
+
else:
|
258
|
+
run_id: str = generated
|
259
|
+
parent_run_id: str = run_id
|
260
|
+
return parent_run_id, run_id
|
261
|
+
|
262
|
+
|
253
263
|
def default_gen_id() -> str:
|
254
264
|
"""Return running ID for making default ID for the Result model.
|
255
265
|
|
@@ -318,12 +328,14 @@ def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
|
318
328
|
)
|
319
329
|
|
320
330
|
|
321
|
-
def cut_id(run_id: str, *, num: int =
|
331
|
+
def cut_id(run_id: str, *, num: int = 8) -> str:
|
322
332
|
"""Cut running ID to specified length.
|
323
333
|
|
324
334
|
Example:
|
325
335
|
>>> cut_id(run_id='20240101081330000000T1354680202')
|
326
336
|
'202401010813680202'
|
337
|
+
>>> cut_id(run_id='20240101081330000000T1354680202')
|
338
|
+
'54680202'
|
327
339
|
|
328
340
|
Args:
|
329
341
|
run_id: A running ID to cut.
|
@@ -334,8 +346,8 @@ def cut_id(run_id: str, *, num: int = 6) -> str:
|
|
334
346
|
"""
|
335
347
|
if "T" in run_id:
|
336
348
|
dt, simple = run_id.split("T", maxsplit=1)
|
337
|
-
return dt[:
|
338
|
-
return run_id[
|
349
|
+
return dt[10:20] + simple[-num:]
|
350
|
+
return run_id[-num:]
|
339
351
|
|
340
352
|
|
341
353
|
@overload
|
@@ -391,3 +403,8 @@ def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
|
|
391
403
|
else:
|
392
404
|
obj_type: str = obj.__class__.__name__
|
393
405
|
return obj_type
|
406
|
+
|
407
|
+
|
408
|
+
def remove_sys_extras(extras: DictData) -> DictData:
|
409
|
+
"""Remove key that starts with `__sys_` from the extra dict parameter."""
|
410
|
+
return {k: extras[k] for k in extras if not k.startswith("__sys_")}
|