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.
@@ -30,19 +30,19 @@ from concurrent.futures import (
30
30
  as_completed,
31
31
  )
32
32
  from datetime import datetime
33
- from enum import Enum
34
33
  from pathlib import Path
35
34
  from queue import Queue
36
35
  from textwrap import dedent
37
36
  from threading import Event as ThreadEvent
38
- from typing import Any, Optional, Union
37
+ from typing import Any, Literal, Optional, Union
39
38
 
40
39
  from pydantic import BaseModel, Field
40
+ from pydantic.functional_serializers import field_serializer
41
41
  from pydantic.functional_validators import field_validator, model_validator
42
42
  from typing_extensions import Self
43
43
 
44
44
  from .__types import DictData
45
- from .audits import Audit, get_audit
45
+ from .audits import NORMAL, RERUN, Audit, ReleaseType, get_audit
46
46
  from .conf import YamlParser, dynamic
47
47
  from .errors import WorkflowCancelError, WorkflowError, WorkflowTimeoutError
48
48
  from .event import Event
@@ -61,40 +61,15 @@ from .result import (
61
61
  validate_statuses,
62
62
  )
63
63
  from .reusables import has_template, param2template
64
- from .traces import TraceManager, get_trace
64
+ from .traces import Trace, get_trace
65
65
  from .utils import (
66
- UTC,
66
+ extract_id,
67
67
  gen_id,
68
68
  get_dt_now,
69
- replace_sec,
69
+ remove_sys_extras,
70
70
  )
71
71
 
72
72
 
73
- class ReleaseType(str, Enum):
74
- """Release type enumeration for workflow execution modes.
75
-
76
- This enum defines the different types of workflow releases that can be
77
- triggered, each with specific behavior and use cases.
78
-
79
- Attributes:
80
- NORMAL: Standard workflow release execution
81
- RERUN: Re-execution of previously failed workflow
82
- EVENT: Event-triggered workflow execution
83
- FORCE: Forced execution bypassing normal conditions
84
- """
85
-
86
- NORMAL = "normal"
87
- RERUN = "rerun"
88
- EVENT = "event"
89
- FORCE = "force"
90
-
91
-
92
- NORMAL = ReleaseType.NORMAL
93
- RERUN = ReleaseType.RERUN
94
- EVENT = ReleaseType.EVENT
95
- FORCE = ReleaseType.FORCE
96
-
97
-
98
73
  class Workflow(BaseModel):
99
74
  """Main workflow orchestration model for job and schedule management.
100
75
 
@@ -113,17 +88,6 @@ class Workflow(BaseModel):
113
88
  on (list[Crontab]): Schedule definitions using cron expressions
114
89
  jobs (dict[str, Job]): Collection of jobs within this workflow
115
90
 
116
- Example:
117
- Create and execute a workflow:
118
-
119
- ```python
120
- workflow = Workflow.from_conf('my-workflow')
121
- result = workflow.execute({
122
- 'param1': 'value1',
123
- 'param2': 'value2'
124
- })
125
- ```
126
-
127
91
  Note:
128
92
  Workflows can be executed immediately or scheduled for background
129
93
  execution using the cron-like scheduling system.
@@ -134,6 +98,7 @@ class Workflow(BaseModel):
134
98
  description="An extra parameters that want to override config values.",
135
99
  )
136
100
  name: str = Field(description="A workflow name.")
101
+ type: Literal["Workflow"] = Field(default="workflow")
137
102
  desc: Optional[str] = Field(
138
103
  default=None,
139
104
  description=(
@@ -198,10 +163,10 @@ class Workflow(BaseModel):
198
163
  FileNotFoundError: If workflow configuration file not found
199
164
 
200
165
  Example:
201
- >>> # Load from default config path
166
+ >>> # NOTE: Load from default config path
202
167
  >>> workflow = Workflow.from_conf('data-pipeline')
203
168
 
204
- >>> # Load with custom path and extras
169
+ >>> # NOTE: Load with custom path and extras
205
170
  >>> workflow = Workflow.from_conf(
206
171
  ... 'data-pipeline',
207
172
  ... path=Path('./custom-configs'),
@@ -211,7 +176,6 @@ class Workflow(BaseModel):
211
176
  load: YamlParser = YamlParser(name, path=path, extras=extras, obj=cls)
212
177
  data: DictData = copy.deepcopy(load.data)
213
178
  data["name"] = name
214
-
215
179
  if extras:
216
180
  data["extras"] = extras
217
181
 
@@ -280,17 +244,29 @@ class Workflow(BaseModel):
280
244
  f"{self.name!r}."
281
245
  )
282
246
 
247
+ # NOTE: Force update internal extras for handler circle execution.
248
+ self.extras.update({"__sys_break_circle_exec": self.name})
249
+
283
250
  return self
284
251
 
252
+ @field_serializer("extras")
253
+ def __serialize_extras(self, extras: DictData) -> DictData:
254
+ return remove_sys_extras(extras)
255
+
285
256
  def detail(self) -> DictData: # pragma: no cov
286
257
  """Return the detail of this workflow for generate markdown."""
287
258
  return self.model_dump(by_alias=True)
288
259
 
289
260
  def md(self, author: Optional[str] = None) -> str: # pragma: no cov
290
- """Generate the markdown template."""
261
+ """Generate the markdown template from this Workflow model data.
262
+
263
+ Args:
264
+ author (str | None, default None): An author name.
265
+ """
291
266
 
292
267
  def align_newline(value: str) -> str:
293
- return value.rstrip("\n").replace("\n", "\n ")
268
+ space: str = " " * 16
269
+ return value.rstrip("\n").replace("\n", f"\n{space}")
294
270
 
295
271
  info: str = (
296
272
  f"| Author: {author or 'nobody'} "
@@ -317,8 +293,7 @@ class Workflow(BaseModel):
317
293
  {align_newline(self.desc)}\n
318
294
  ## Parameters\n
319
295
  | name | type | default | description |
320
- | --- | --- | --- | : --- : |
321
-
296
+ | --- | --- | --- | : --- : |\n\n
322
297
  ## Jobs\n
323
298
  {align_newline(jobs)}
324
299
  """.lstrip(
@@ -347,8 +322,7 @@ class Workflow(BaseModel):
347
322
  f"{self.name!r}"
348
323
  )
349
324
  job: Job = self.jobs[name]
350
- if self.extras:
351
- job.extras = self.extras
325
+ job.extras = self.extras
352
326
  return job
353
327
 
354
328
  def parameterize(self, params: DictData) -> DictData:
@@ -367,8 +341,8 @@ class Workflow(BaseModel):
367
341
  execute method.
368
342
 
369
343
  Returns:
370
- DictData: The parameter value that validate with its parameter fields and
371
- adding jobs key to this parameter.
344
+ DictData: The parameter value that validate with its parameter fields
345
+ and adding jobs key to this parameter.
372
346
 
373
347
  Raises:
374
348
  WorkflowError: If parameter value that want to validate does
@@ -399,33 +373,6 @@ class Workflow(BaseModel):
399
373
  "jobs": {},
400
374
  }
401
375
 
402
- def validate_release(self, dt: datetime) -> datetime:
403
- """Validate the release datetime that should was replaced second and
404
- millisecond to 0 and replaced timezone to None before checking it match
405
- with the set `on` field.
406
-
407
- Args:
408
- dt (datetime): A datetime object that want to validate.
409
-
410
- Returns:
411
- datetime: The validated release datetime.
412
- """
413
- if dt.tzinfo is None:
414
- dt = dt.replace(tzinfo=UTC)
415
-
416
- release: datetime = replace_sec(dt.astimezone(UTC))
417
-
418
- # NOTE: Return itself if schedule event does not set.
419
- if not self.on.schedule:
420
- return release
421
-
422
- for on in self.on.schedule:
423
- if release == on.cronjob.schedule(release, tz=UTC).next:
424
- return release
425
- raise WorkflowError(
426
- "Release datetime does not support for this workflow"
427
- )
428
-
429
376
  def release(
430
377
  self,
431
378
  release: datetime,
@@ -437,7 +384,7 @@ class Workflow(BaseModel):
437
384
  override_log_name: Optional[str] = None,
438
385
  timeout: int = 600,
439
386
  audit_excluded: Optional[list[str]] = None,
440
- audit: type[Audit] = None,
387
+ audit: Audit = None,
441
388
  ) -> Result:
442
389
  """Release the workflow which is executes workflow with writing audit
443
390
  log tracking. The method is overriding parameter with the release
@@ -455,11 +402,12 @@ class Workflow(BaseModel):
455
402
 
456
403
  Args:
457
404
  release (datetime): A release datetime.
458
- params: A workflow parameter that pass to execute method.
459
- release_type:
405
+ params (DictData): A workflow parameter that pass to execute method.
406
+ release_type (ReleaseType): A release type that want to execute.
460
407
  run_id: (str) A workflow running ID.
461
408
  runs_metadata: (DictData)
462
- audit: An audit class that want to save the execution result.
409
+ audit (Audit): An audit model that use to manage release log of this
410
+ execution.
463
411
  override_log_name: (str) An override logging name that use
464
412
  instead the workflow name.
465
413
  timeout: (int) A workflow execution time out in second unit.
@@ -471,20 +419,25 @@ class Workflow(BaseModel):
471
419
  method.
472
420
  """
473
421
  name: str = override_log_name or self.name
422
+ audit: Audit = audit or get_audit(extras=self.extras)
474
423
 
475
424
  # NOTE: Generate the parent running ID with not None value.
476
- if run_id:
477
- parent_run_id: str = run_id
478
- run_id: str = gen_id(name, unique=True)
479
- else:
480
- run_id: str = gen_id(name, unique=True)
481
- parent_run_id: str = run_id
482
-
425
+ parent_run_id, run_id = extract_id(
426
+ name, run_id=run_id, extras=self.extras
427
+ )
483
428
  context: DictData = {"status": WAIT}
484
- trace: TraceManager = get_trace(
429
+ audit_data: DictData = {
430
+ "name": name,
431
+ "release": release,
432
+ "type": release_type,
433
+ "run_id": run_id,
434
+ "parent_run_id": parent_run_id,
435
+ "extras": self.extras,
436
+ }
437
+ trace: Trace = get_trace(
485
438
  run_id, parent_run_id=parent_run_id, extras=self.extras
486
439
  )
487
- release: datetime = self.validate_release(dt=release)
440
+ release: datetime = self.on.validate_dt(dt=release)
488
441
  trace.info(f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}")
489
442
  values: DictData = param2template(
490
443
  params,
@@ -498,6 +451,25 @@ class Workflow(BaseModel):
498
451
  },
499
452
  extras=self.extras,
500
453
  )
454
+
455
+ if release_type == NORMAL and audit.is_pointed(data=audit_data):
456
+ trace.info("[RELEASE]: Skip this release because it already audit.")
457
+ return Result(
458
+ run_id=run_id,
459
+ parent_run_id=parent_run_id,
460
+ status=SKIP,
461
+ context=catch(context, status=SKIP),
462
+ extras=self.extras,
463
+ )
464
+
465
+ if release_type == RERUN:
466
+ # TODO: It will load previous audit and use this data to run with
467
+ # the `rerun` method.
468
+ raise NotImplementedError(
469
+ "Release does not support for rerun type yet. Please use the "
470
+ "`rerun` method instead."
471
+ )
472
+
501
473
  rs: Result = self.execute(
502
474
  params=values,
503
475
  run_id=parent_run_id,
@@ -507,15 +479,10 @@ class Workflow(BaseModel):
507
479
  trace.info(f"[RELEASE]: End {name!r} : {release:%Y-%m-%d %H:%M:%S}")
508
480
  trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
509
481
  (
510
- (audit or get_audit(extras=self.extras)).save(
511
- data={
512
- "name": name,
513
- "release": release,
514
- "type": release_type,
482
+ audit.save(
483
+ data=audit_data
484
+ | {
515
485
  "context": context,
516
- "parent_run_id": parent_run_id,
517
- "run_id": run_id,
518
- "extras": self.extras,
519
486
  "runs_metadata": (
520
487
  (runs_metadata or {})
521
488
  | rs.info
@@ -546,7 +513,7 @@ class Workflow(BaseModel):
546
513
  **(context["errors"] if "errors" in context else {}),
547
514
  },
548
515
  ),
549
- extras=self.extras,
516
+ extras=remove_sys_extras(self.extras),
550
517
  )
551
518
 
552
519
  def execute_job(
@@ -579,7 +546,7 @@ class Workflow(BaseModel):
579
546
  Returns:
580
547
  tuple[Status, DictData]: The pair of status and result context data.
581
548
  """
582
- trace: TraceManager = get_trace(
549
+ trace: Trace = get_trace(
583
550
  run_id, parent_run_id=parent_run_id, extras=self.extras
584
551
  )
585
552
  if event and event.is_set():
@@ -694,9 +661,10 @@ class Workflow(BaseModel):
694
661
  :rtype: Result
695
662
  """
696
663
  ts: float = time.monotonic()
697
- parent_run_id: Optional[str] = run_id
698
- run_id: str = gen_id(self.name, extras=self.extras)
699
- trace: TraceManager = get_trace(
664
+ parent_run_id, run_id = extract_id(
665
+ self.name, run_id=run_id, extras=self.extras
666
+ )
667
+ trace: Trace = get_trace(
700
668
  run_id, parent_run_id=parent_run_id, extras=self.extras
701
669
  )
702
670
  context: DictData = self.parameterize(params)
@@ -733,6 +701,11 @@ class Workflow(BaseModel):
733
701
  )
734
702
  catch(context, status=WAIT)
735
703
  if event and event.is_set():
704
+ err_msg: str = (
705
+ "Execution was canceled from the event was set "
706
+ "before workflow execution."
707
+ )
708
+ trace.error(f"[WORKFLOW]: {err_msg}")
736
709
  return Result(
737
710
  run_id=run_id,
738
711
  parent_run_id=parent_run_id,
@@ -740,12 +713,7 @@ class Workflow(BaseModel):
740
713
  context=catch(
741
714
  context,
742
715
  status=CANCEL,
743
- updated={
744
- "errors": WorkflowCancelError(
745
- "Execution was canceled from the event was set "
746
- "before workflow execution."
747
- ).to_dict(),
748
- },
716
+ updated={"errors": WorkflowCancelError(err_msg).to_dict()},
749
717
  ),
750
718
  info={"execution_time": time.monotonic() - ts},
751
719
  extras=self.extras,
@@ -799,7 +767,7 @@ class Workflow(BaseModel):
799
767
  )
800
768
  elif check == SKIP: # pragma: no cov
801
769
  trace.info(
802
- f"[JOB]: Skip job: {job_id!r} from trigger rule."
770
+ f"[JOB]: ⏭️ Skip job: {job_id!r} from trigger rule."
803
771
  )
804
772
  job.set_outputs(output={"status": SKIP}, to=context)
805
773
  job_queue.task_done()
@@ -923,16 +891,21 @@ class Workflow(BaseModel):
923
891
  ) -> Result: # pragma: no cov
924
892
  """Re-Execute workflow with passing the error context data.
925
893
 
926
- :param context: A context result that get the failed status.
927
- :param run_id: (Optional[str]) A workflow running ID.
928
- :param event: (Event) An Event manager instance that use to cancel this
929
- execution if it forces stopped by parent execution.
930
- :param timeout: (float) A workflow execution time out in second unit
931
- that use for limit time of execution and waiting job dependency.
932
- This value does not force stop the task that still running more than
933
- this limit time. (Default: 60 * 60 seconds)
934
- :param max_job_parallel: (int) The maximum workers that use for job
935
- execution in `ThreadPoolExecutor` object. (Default: 2 workers)
894
+ Warnings:
895
+ This rerun method allow to rerun job execution level only. That mean
896
+ it does not support rerun only stage.
897
+
898
+ Args:
899
+ context: A context result that get the failed status.
900
+ run_id: (Optional[str]) A workflow running ID.
901
+ event: (Event) An Event manager instance that use to cancel this
902
+ execution if it forces stopped by parent execution.
903
+ timeout: (float) A workflow execution time out in second unit
904
+ that use for limit time of execution and waiting job dependency.
905
+ This value does not force stop the task that still running more
906
+ than this limit time. (Default: 60 * 60 seconds)
907
+ max_job_parallel: (int) The maximum workers that use for job
908
+ execution in `ThreadPoolExecutor` object. (Default: 2 workers)
936
909
 
937
910
  Returns
938
911
  Result: Return Result object that create from execution context with
@@ -941,7 +914,7 @@ class Workflow(BaseModel):
941
914
  ts: float = time.monotonic()
942
915
  parent_run_id: str = run_id
943
916
  run_id: str = gen_id(self.name, extras=self.extras)
944
- trace: TraceManager = get_trace(
917
+ trace: Trace = get_trace(
945
918
  run_id, parent_run_id=parent_run_id, extras=self.extras
946
919
  )
947
920
  if context["status"] == SUCCESS:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.81
3
+ Version: 0.0.83
4
4
  Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,35 @@
1
+ ddeutil/workflow/__about__.py,sha256=6n_dCde_BwTTJlP4Yu4HNgiRti7QydfShki703lt6Ro,60
2
+ ddeutil/workflow/__cron.py,sha256=-1tqZG7GtUmusdl6NTy_Ck7nM_tGYTXYB7TB7tKeO60,29184
3
+ ddeutil/workflow/__init__.py,sha256=Dvfjs7LpLerGCYGnbqKwznViTw7ire_6LR8obC1I4aM,3456
4
+ ddeutil/workflow/__main__.py,sha256=Nqk5aO-HsZVKV2BmuJYeJEufJluipvCD9R1k2kMoJ3Y,8581
5
+ ddeutil/workflow/__types.py,sha256=IOKuJCxTUPHh8Z2JoLu_K7a85oq0VOcKBhpabiJ6qEE,5001
6
+ ddeutil/workflow/audits.py,sha256=H8yuMzXs_QAAKNox-HXdojk9CcilHYYQtklJYetoZv8,26955
7
+ ddeutil/workflow/conf.py,sha256=VfPmwaBYEgOj8bu4eim13ayZwJ4Liy7I702aQf7vS8g,17644
8
+ ddeutil/workflow/errors.py,sha256=J4bEbtI7qtBX7eghod4djLf0y5i1r4mCz_uFU4roLhY,5713
9
+ ddeutil/workflow/event.py,sha256=OumcZBlOZD0_J53GS4V2XJEqQ9HEcIl3UicQrCyL46M,14684
10
+ ddeutil/workflow/job.py,sha256=VVTpxVR2iVEkjvP8r0O0LRtAPnrbsguYbKzHpe2TAVo,48146
11
+ ddeutil/workflow/params.py,sha256=y9f6DEIyae1j4awbj3Kbeq75-U2UPFlKv9K57Hdo_Go,17188
12
+ ddeutil/workflow/result.py,sha256=0W3z5wAs3Dyr8r2vRMY5hl1MkvdsyXWJmQD4NmsDDOM,10194
13
+ ddeutil/workflow/reusables.py,sha256=SBLJSxR8ELoWJErBfSMZS3Rr1O_93T-fFBpfn2AvxuA,25007
14
+ ddeutil/workflow/stages.py,sha256=lWlzvpJ6YyhDf0ks5q_fzHjm4-o6UZfhiYp9CG-ffro,129661
15
+ ddeutil/workflow/traces.py,sha256=pq1lOg2UMgDiSDmjHxXPoTaBHnfc7uzzlo1u2TCwN2Q,74733
16
+ ddeutil/workflow/utils.py,sha256=XsH8DkcTiMmWt1e59b4bFQofsBdo7uW1-7gC2rghuW8,12128
17
+ ddeutil/workflow/workflow.py,sha256=uc71PJh7e-Bjb3Xg7T83wlLrKTPGvmX7Qjsn6SJ1GDI,42544
18
+ ddeutil/workflow/api/__init__.py,sha256=5DzYL3ngceoRshh5HYCSVWChqNJSiP01E1bEd8XxPi0,4799
19
+ ddeutil/workflow/api/log_conf.py,sha256=WfS3udDLSyrP-C80lWOvxxmhd_XWKvQPkwDqKblcH3E,1834
20
+ ddeutil/workflow/api/routes/__init__.py,sha256=JRaJZB0D6mgR17MbZo8yLtdYDtD62AA8MdKlFqhG84M,420
21
+ ddeutil/workflow/api/routes/job.py,sha256=-lbZ_hS9pEdSy6zeke5qrXEgdNxtQ2w9in7cHuM2Jzs,2536
22
+ ddeutil/workflow/api/routes/logs.py,sha256=9jiYsw8kepud4n3NyXB7SAr2OoQwRn5uNb9kIZ58XJM,3806
23
+ ddeutil/workflow/api/routes/workflows.py,sha256=0pEZEsIrscRFBXG9gf6nttKw0aNbcdw7NsAZKLoKWtk,4392
24
+ ddeutil/workflow/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
+ ddeutil/workflow/plugins/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ ddeutil/workflow/plugins/providers/aws.py,sha256=61uIFBEWt-_D5Sui24qUPier1Hiqlw_RP_eY-rXBCKc,31551
27
+ ddeutil/workflow/plugins/providers/az.py,sha256=o3dh011lEtmr7-d7FPZJPgXdT0ytFzKfc5xnVxSyXGU,34867
28
+ ddeutil/workflow/plugins/providers/container.py,sha256=DSN0RWxMjTJN5ANheeMauDaPa3X6Z2E1eGUcctYkENw,22134
29
+ ddeutil/workflow/plugins/providers/gcs.py,sha256=KgAOdMBvdbMLTH_z_FwVriBFtZfKEYx8_34jzUOVjTY,27460
30
+ ddeutil_workflow-0.0.83.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
31
+ ddeutil_workflow-0.0.83.dist-info/METADATA,sha256=pxD6FyTSV4ra5DujoPvWQnb2Z9WWHHqi97HBADqeeAo,16087
32
+ ddeutil_workflow-0.0.83.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ ddeutil_workflow-0.0.83.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
34
+ ddeutil_workflow-0.0.83.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
35
+ ddeutil_workflow-0.0.83.dist-info/RECORD,,