ddeutil-workflow 0.0.59__py3-none-any.whl → 0.0.61__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.59"
1
+ __version__: str = "0.0.61"
@@ -20,6 +20,7 @@ from typing import Any, Optional, TypedDict, Union
20
20
 
21
21
  from typing_extensions import Self
22
22
 
23
+ StrOrNone = Optional[str]
23
24
  StrOrInt = Union[str, int]
24
25
  TupleStr = tuple[str, ...]
25
26
  DictData = dict[str, Any]
@@ -42,7 +43,7 @@ class CallerRe:
42
43
 
43
44
  full: str
44
45
  caller: str
45
- caller_prefix: Optional[str]
46
+ caller_prefix: StrOrNone
46
47
  caller_last: str
47
48
  post_filters: str
48
49
 
@@ -50,6 +51,9 @@ class CallerRe:
50
51
  def from_regex(cls, match: Match[str]) -> Self:
51
52
  """Class construct from matching result.
52
53
 
54
+ :param match: A match string object for contract this Caller regex data
55
+ class.
56
+
53
57
  :rtype: Self
54
58
  """
55
59
  return cls(full=match.group(0), **match.groupdict())
@@ -121,10 +125,13 @@ class Re:
121
125
  )
122
126
 
123
127
  @classmethod
124
- def finditer_caller(cls, value) -> Iterator[CallerRe]:
128
+ def finditer_caller(cls, value: str) -> Iterator[CallerRe]:
125
129
  """Generate CallerRe object that create from matching object that
126
130
  extract with re.finditer function.
127
131
 
132
+ :param value: (str) A string value that want to finditer with the caller
133
+ regular expression.
134
+
128
135
  :rtype: Iterator[CallerRe]
129
136
  """
130
137
  for found in cls.RE_CALLER.finditer(value):
ddeutil/workflow/event.py CHANGED
@@ -3,8 +3,9 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Event module that store all event object. Now, it has only `Crontab` and
7
- `CrontabYear` model these are schedule with crontab event.
6
+ """Event module include all event object for trigger the Workflow to release.
7
+ Now, it has only `Crontab` and `CrontabYear` event models on this module because
8
+ I think it is the core event for workflow orchestration.
8
9
  """
9
10
  from __future__ import annotations
10
11
 
@@ -86,7 +87,7 @@ class Crontab(BaseModel):
86
87
  CronJob,
87
88
  Field(
88
89
  description=(
89
- "A Cronjob object that use for validate and generate datetime.",
90
+ "A Cronjob object that use for validate and generate datetime."
90
91
  ),
91
92
  ),
92
93
  ]
@@ -117,7 +118,6 @@ class Crontab(BaseModel):
117
118
  passing["cronjob"] = interval2crontab(
118
119
  **{v: value[v] for v in value if v in ("interval", "day", "time")}
119
120
  )
120
- print(passing)
121
121
  return cls(extras=extras | passing.pop("extras", {}), **passing)
122
122
 
123
123
  @classmethod
@@ -170,9 +170,10 @@ class Crontab(BaseModel):
170
170
 
171
171
  @model_validator(mode="before")
172
172
  def __prepare_values(cls, data: Any) -> Any:
173
- """Extract tz key from value and change name to timezone key.
173
+ """Extract a `tz` key from data and change the key name from `tz` to
174
+ `timezone`.
174
175
 
175
- :param data: (DictData) A data that want to pass for create an Crontab
176
+ :param data: (DictData) A data that want to pass for create a Crontab
176
177
  model.
177
178
 
178
179
  :rtype: DictData
@@ -198,7 +199,7 @@ class Crontab(BaseModel):
198
199
  "cronjob", mode="before", json_schema_input_type=Union[CronJob, str]
199
200
  )
200
201
  def __prepare_cronjob(
201
- cls, value: str | CronJob, info: ValidationInfo
202
+ cls, value: Union[str, CronJob], info: ValidationInfo
202
203
  ) -> CronJob:
203
204
  """Prepare crontab value that able to receive with string type.
204
205
  This step will get options kwargs from extras field and pass to the
@@ -234,7 +235,7 @@ class Crontab(BaseModel):
234
235
  """
235
236
  return str(value)
236
237
 
237
- def generate(self, start: str | datetime) -> CronRunner:
238
+ def generate(self, start: Union[str, datetime]) -> CronRunner:
238
239
  """Return CronRunner object from an initial datetime.
239
240
 
240
241
  :param start: (str | datetime) A string or datetime for generate the
@@ -248,7 +249,7 @@ class Crontab(BaseModel):
248
249
  raise TypeError("start value should be str or datetime type.")
249
250
  return self.cronjob.schedule(date=start, tz=self.tz)
250
251
 
251
- def next(self, start: str | datetime) -> CronRunner:
252
+ def next(self, start: Union[str, datetime]) -> CronRunner:
252
253
  """Return a next datetime from Cron runner object that start with any
253
254
  date that given from input.
254
255
 
@@ -277,16 +278,18 @@ class CrontabYear(Crontab):
277
278
  CronJobYear,
278
279
  Field(
279
280
  description=(
280
- "A Cronjob object that use for validate and generate datetime.",
281
+ "A Cronjob object that use for validate and generate datetime."
281
282
  ),
282
283
  ),
283
284
  ]
284
285
 
285
286
  @field_validator(
286
- "cronjob", mode="before", json_schema_input_type=Union[CronJobYear, str]
287
+ "cronjob",
288
+ mode="before",
289
+ json_schema_input_type=Union[CronJobYear, str],
287
290
  )
288
291
  def __prepare_cronjob(
289
- cls, value: str | CronJobYear, info: ValidationInfo
292
+ cls, value: Union[CronJobYear, str], info: ValidationInfo
290
293
  ) -> CronJobYear:
291
294
  """Prepare crontab value that able to receive with string type.
292
295
  This step will get options kwargs from extras field and pass to the
@@ -311,3 +314,15 @@ class CrontabYear(Crontab):
311
314
  if isinstance(value, str)
312
315
  else value
313
316
  )
317
+
318
+
319
+ Event = Annotated[
320
+ Union[
321
+ CronJobYear,
322
+ CronJob,
323
+ ],
324
+ Field(
325
+ union_mode="smart",
326
+ description="An event models.",
327
+ ),
328
+ ] # pragma: no cov
@@ -9,7 +9,7 @@ annotate for handle error only.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
- from typing import Literal, Optional, TypedDict, overload
12
+ from typing import Literal, Optional, TypedDict, Union, overload
13
13
 
14
14
 
15
15
  class ErrorData(TypedDict):
@@ -55,8 +55,9 @@ class BaseWorkflowException(Exception):
55
55
 
56
56
  def to_dict(
57
57
  self, with_refs: bool = False
58
- ) -> ErrorData | dict[str, ErrorData]:
59
- """Return ErrorData data from the current exception object.
58
+ ) -> Union[ErrorData, dict[str, ErrorData]]:
59
+ """Return ErrorData data from the current exception object. If with_refs
60
+ flag was set, it will return mapping of refs and itself data.
60
61
 
61
62
  :rtype: ErrorData
62
63
  """
ddeutil/workflow/job.py CHANGED
@@ -39,7 +39,7 @@ from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
39
39
  from pydantic.functional_validators import field_validator, model_validator
40
40
  from typing_extensions import Self
41
41
 
42
- from .__types import DictData, DictStr, Matrix
42
+ from .__types import DictData, DictStr, Matrix, StrOrNone
43
43
  from .exceptions import (
44
44
  JobException,
45
45
  StageException,
@@ -329,14 +329,14 @@ class Job(BaseModel):
329
329
  ... }
330
330
  """
331
331
 
332
- id: Optional[str] = Field(
332
+ id: StrOrNone = Field(
333
333
  default=None,
334
334
  description=(
335
335
  "A job ID that was set from Workflow model after initialize step. "
336
336
  "If this model create standalone, it will be None."
337
337
  ),
338
338
  )
339
- desc: Optional[str] = Field(
339
+ desc: StrOrNone = Field(
340
340
  default=None,
341
341
  description="A job description that can be markdown syntax.",
342
342
  )
@@ -345,7 +345,7 @@ class Job(BaseModel):
345
345
  description="A target node for this job to use for execution.",
346
346
  alias="runs-on",
347
347
  )
348
- condition: Optional[str] = Field(
348
+ condition: StrOrNone = Field(
349
349
  default=None,
350
350
  description="A job condition statement to allow job executable.",
351
351
  alias="if",
@@ -526,7 +526,7 @@ class Job(BaseModel):
526
526
  output: DictData,
527
527
  to: DictData,
528
528
  *,
529
- job_id: Optional[str] = None,
529
+ job_id: StrOrNone = None,
530
530
  ) -> DictData:
531
531
  """Set an outputs from execution result context to the received context
532
532
  with a `to` input parameter. The result context from job strategy
@@ -567,7 +567,7 @@ class Job(BaseModel):
567
567
  :param output: (DictData) A result data context that want to extract
568
568
  and transfer to the `strategies` key in receive context.
569
569
  :param to: (DictData) A received context data.
570
- :param job_id: (Optional[str]) A job ID if the `id` field does not set.
570
+ :param job_id: (StrOrNone) A job ID if the `id` field does not set.
571
571
 
572
572
  :rtype: DictData
573
573
  """
@@ -607,8 +607,8 @@ class Job(BaseModel):
607
607
  self,
608
608
  params: DictData,
609
609
  *,
610
- run_id: Optional[str] = None,
611
- parent_run_id: Optional[str] = None,
610
+ run_id: StrOrNone = None,
611
+ parent_run_id: StrOrNone = None,
612
612
  event: Optional[Event] = None,
613
613
  ) -> Result:
614
614
  """Job execution with passing dynamic parameters from the workflow
@@ -800,8 +800,8 @@ def local_execute(
800
800
  job: Job,
801
801
  params: DictData,
802
802
  *,
803
- run_id: Optional[str] = None,
804
- parent_run_id: Optional[str] = None,
803
+ run_id: StrOrNone = None,
804
+ parent_run_id: StrOrNone = None,
805
805
  event: Optional[Event] = None,
806
806
  ) -> Result:
807
807
  """Local job execution with passing dynamic parameters from the workflow
@@ -919,8 +919,8 @@ def self_hosted_execute(
919
919
  job: Job,
920
920
  params: DictData,
921
921
  *,
922
- run_id: Optional[str] = None,
923
- parent_run_id: Optional[str] = None,
922
+ run_id: StrOrNone = None,
923
+ parent_run_id: StrOrNone = None,
924
924
  event: Optional[Event] = None,
925
925
  ) -> Result: # pragma: no cov
926
926
  """Self-Hosted job execution with passing dynamic parameters from the
@@ -982,8 +982,8 @@ def azure_batch_execute(
982
982
  job: Job,
983
983
  params: DictData,
984
984
  *,
985
- run_id: Optional[str] = None,
986
- parent_run_id: Optional[str] = None,
985
+ run_id: StrOrNone = None,
986
+ parent_run_id: StrOrNone = None,
987
987
  event: Optional[Event] = None,
988
988
  ) -> Result: # pragma: no cov
989
989
  """Azure Batch job execution that will run all job's stages on the Azure
@@ -1036,8 +1036,8 @@ def docker_execution(
1036
1036
  job: Job,
1037
1037
  params: DictData,
1038
1038
  *,
1039
- run_id: Optional[str] = None,
1040
- parent_run_id: Optional[str] = None,
1039
+ run_id: StrOrNone = None,
1040
+ parent_run_id: StrOrNone = None,
1041
1041
  event: Optional[Event] = None,
1042
1042
  ): # pragma: no cov
1043
1043
  """Docker job execution.
ddeutil/workflow/logs.py CHANGED
@@ -4,13 +4,17 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  # [x] Use fix config for `get_logger`, and Model initialize step.
7
- """A Logs module contain Trace dataclass and Audit Pydantic model.
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.
8
11
  """
9
12
  from __future__ import annotations
10
13
 
11
14
  import json
12
15
  import logging
13
16
  import os
17
+ import re
14
18
  from abc import ABC, abstractmethod
15
19
  from collections.abc import Iterator
16
20
  from datetime import datetime
@@ -18,7 +22,8 @@ from functools import lru_cache
18
22
  from inspect import Traceback, currentframe, getframeinfo
19
23
  from pathlib import Path
20
24
  from threading import get_ident
21
- from typing import ClassVar, Literal, Optional, TypeVar, Union
25
+ from types import FrameType
26
+ from typing import ClassVar, Final, Literal, Optional, TypeVar, Union
22
27
 
23
28
  from pydantic import BaseModel, ConfigDict, Field
24
29
  from pydantic.functional_validators import model_validator
@@ -28,12 +33,14 @@ from .__types import DictData
28
33
  from .conf import config, dynamic
29
34
  from .utils import cut_id, get_dt_now, prepare_newline
30
35
 
36
+ METADATA: str = "metadata.json"
37
+
31
38
 
32
39
  @lru_cache
33
40
  def get_logger(name: str):
34
41
  """Return logger object with an input module name.
35
42
 
36
- :param name: A module name that want to log.
43
+ :param name: (str) A module name that want to log.
37
44
  """
38
45
  lg = logging.getLogger(name)
39
46
 
@@ -67,14 +74,59 @@ def get_dt_tznow() -> datetime: # pragma: no cov
67
74
  return get_dt_now(tz=config.tz)
68
75
 
69
76
 
70
- PREFIX_LOGS: dict[str, dict] = {
71
- "CALLER": {"emoji": ""},
72
- "STAGE": {"emoji": ""},
73
- "JOB": {"emoji": ""},
74
- "WORKFLOW": {"emoji": "🏃"},
75
- "RELEASE": {"emoji": ""},
76
- "POKE": {"emoji": ""},
77
+ PREFIX_LOGS: Final[dict[str, dict]] = {
78
+ "CALLER": {
79
+ "emoji": "📍",
80
+ "desc": "logs from any usage from custom caller function.",
81
+ },
82
+ "STAGE": {"emoji": "⚙️", "desc": "logs from stages module."},
83
+ "JOB": {"emoji": "⛓️", "desc": "logs from job module."},
84
+ "WORKFLOW": {"emoji": "🏃", "desc": "logs from workflow module."},
85
+ "RELEASE": {"emoji": "📅", "desc": "logs from release workflow method."},
86
+ "POKING": {"emoji": "⏰", "desc": "logs from poke workflow method."},
77
87
  } # pragma: no cov
88
+ PREFIX_DEFAULT: Final[str] = "CALLER"
89
+ PREFIX_LOGS_REGEX: re.Pattern[str] = re.compile(
90
+ rf"(^\[(?P<name>{'|'.join(PREFIX_LOGS)})]:\s?)?(?P<message>.*)",
91
+ re.MULTILINE | re.DOTALL | re.ASCII | re.VERBOSE,
92
+ ) # pragma: no cov
93
+
94
+
95
+ class PrefixMsg(BaseModel):
96
+ """Prefix Message model for receive grouping dict from searching prefix data
97
+ from logging message.
98
+ """
99
+
100
+ name: Optional[str] = Field(default=None)
101
+ message: Optional[str] = Field(default=None)
102
+
103
+ def prepare(self, extras: Optional[DictData] = None) -> str:
104
+ """Prepare message with force add prefix before writing trace log.
105
+
106
+ :param extras: (DictData) An extra parameter that want to get the
107
+ `log_add_emoji` flag.
108
+
109
+ :rtype: str
110
+ """
111
+ name: str = self.name or PREFIX_DEFAULT
112
+ emoji: str = (
113
+ f"{PREFIX_LOGS[name]['emoji']} "
114
+ if (extras or {}).get("log_add_emoji", True)
115
+ else ""
116
+ )
117
+ return f"{emoji}[{name}]: {self.message}"
118
+
119
+
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
+ )
78
130
 
79
131
 
80
132
  class TraceMeta(BaseModel): # pragma: no cov
@@ -91,6 +143,28 @@ class TraceMeta(BaseModel): # pragma: no cov
91
143
  filename: str = Field(description="A filename of this log.")
92
144
  lineno: int = Field(description="A line number of this log.")
93
145
 
146
+ @classmethod
147
+ def dynamic_frame(
148
+ cls, frame: FrameType, *, extras: Optional[DictData] = None
149
+ ) -> Traceback:
150
+ """Dynamic Frame information base on the `logs_trace_frame_layer` config
151
+ value that was set from the extra parameter.
152
+
153
+ :param frame: (FrameType) The current frame that want to dynamic.
154
+ :param extras: (DictData) An extra parameter that want to get the
155
+ `logs_trace_frame_layer` config value.
156
+ """
157
+ extras: DictData = extras or {}
158
+ layer: int = extras.get("logs_trace_frame_layer", 4)
159
+ for _ in range(layer):
160
+ _frame: Optional[FrameType] = frame.f_back
161
+ if _frame is None:
162
+ raise ValueError(
163
+ f"Layer value does not valid, the maximum frame is: {_ + 1}"
164
+ )
165
+ frame: FrameType = _frame
166
+ return getframeinfo(frame)
167
+
94
168
  @classmethod
95
169
  def make(
96
170
  cls,
@@ -100,7 +174,8 @@ class TraceMeta(BaseModel): # pragma: no cov
100
174
  *,
101
175
  extras: Optional[DictData] = None,
102
176
  ) -> Self:
103
- """Make the current TraceMeta instance that catching local state.
177
+ """Make the current metric for contract this TraceMeta model instance
178
+ that will catch local states like PID, thread identity.
104
179
 
105
180
  :param mode: (Literal["stdout", "stderr"]) A metadata mode.
106
181
  :param message: (str) A message.
@@ -110,9 +185,8 @@ class TraceMeta(BaseModel): # pragma: no cov
110
185
 
111
186
  :rtype: Self
112
187
  """
113
- frame_info: Traceback = getframeinfo(
114
- currentframe().f_back.f_back.f_back
115
- )
188
+ frame: FrameType = currentframe()
189
+ frame_info: Traceback = cls.dynamic_frame(frame, extras=extras)
116
190
  extras: DictData = extras or {}
117
191
  return cls(
118
192
  mode=mode,
@@ -157,13 +231,11 @@ class TraceData(BaseModel): # pragma: no cov
157
231
  if (file / f"{mode}.txt").exists():
158
232
  data[mode] = (file / f"{mode}.txt").read_text(encoding="utf-8")
159
233
 
160
- if (file / "metadata.json").exists():
234
+ if (file / METADATA).exists():
161
235
  data["meta"] = [
162
236
  json.loads(line)
163
237
  for line in (
164
- (file / "metadata.json")
165
- .read_text(encoding="utf-8")
166
- .splitlines()
238
+ (file / METADATA).read_text(encoding="utf-8").splitlines()
167
239
  )
168
240
  ]
169
241
 
@@ -263,7 +335,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
263
335
 
264
336
  :param message: (str) A message that want to log.
265
337
  """
266
- msg: str = prepare_newline(self.make_message(message))
338
+ msg: str = self.make_message(message)
267
339
 
268
340
  if mode != "debug" or (
269
341
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -320,7 +392,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
320
392
 
321
393
  :param message: (str) A message that want to log.
322
394
  """
323
- msg: str = prepare_newline(self.make_message(message))
395
+ msg: str = self.make_message(message)
324
396
 
325
397
  if mode != "debug" or (
326
398
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -441,13 +513,15 @@ class FileTrace(BaseTrace): # pragma: no cov
441
513
  return f"{cut_parent_run_id} -> {cut_run_id}"
442
514
 
443
515
  def make_message(self, message: str) -> str:
444
- """Prepare and Make a message before write and log processes.
516
+ """Prepare and Make a message before write and log steps.
445
517
 
446
518
  :param message: (str) A message that want to prepare and make before.
447
519
 
448
520
  :rtype: str
449
521
  """
450
- return f"({self.cut_id}) {message}"
522
+ return prepare_newline(
523
+ f"({self.cut_id}) {extract_msg_prefix(message).prepare(self.extras)}"
524
+ )
451
525
 
452
526
  def writer(self, message: str, level: str, is_err: bool = False) -> None:
453
527
  """Write a trace message after making to target file and write metadata
@@ -468,7 +542,7 @@ class FileTrace(BaseTrace): # pragma: no cov
468
542
 
469
543
  mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
470
544
  trace_meta: TraceMeta = TraceMeta.make(
471
- mode=mode, level=level, message=message
545
+ mode=mode, level=level, message=message, extras=self.extras
472
546
  )
473
547
 
474
548
  with (self.pointer / f"{mode}.txt").open(
@@ -477,9 +551,7 @@ class FileTrace(BaseTrace): # pragma: no cov
477
551
  fmt: str = dynamic("log_format_file", extras=self.extras)
478
552
  f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
479
553
 
480
- with (self.pointer / "metadata.json").open(
481
- mode="at", encoding="utf-8"
482
- ) as f:
554
+ with (self.pointer / METADATA).open(mode="at", encoding="utf-8") as f:
483
555
  f.write(trace_meta.model_dump_json() + "\n")
484
556
 
485
557
  async def awriter(
@@ -496,7 +568,7 @@ class FileTrace(BaseTrace): # pragma: no cov
496
568
 
497
569
  mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
498
570
  trace_meta: TraceMeta = TraceMeta.make(
499
- mode=mode, level=level, message=message
571
+ mode=mode, level=level, message=message, extras=self.extras
500
572
  )
501
573
 
502
574
  async with aiofiles.open(
@@ -506,7 +578,7 @@ class FileTrace(BaseTrace): # pragma: no cov
506
578
  await f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
507
579
 
508
580
  async with aiofiles.open(
509
- self.pointer / "metadata.json", mode="at", encoding="utf-8"
581
+ self.pointer / METADATA, mode="at", encoding="utf-8"
510
582
  ) as f:
511
583
  await f.write(trace_meta.model_dump_json() + "\n")
512
584