ddeutil-workflow 0.0.82__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.
@@ -51,6 +51,8 @@ from .utils import cut_id, get_dt_now, prepare_newline
51
51
 
52
52
  logger = logging.getLogger("ddeutil.workflow")
53
53
  Level = Literal["debug", "info", "warning", "error", "exception"]
54
+ EMJ_ALERT: str = "🚨"
55
+ EMJ_SKIP: str = "⏭️"
54
56
 
55
57
 
56
58
  @lru_cache
@@ -239,7 +241,7 @@ class Metadata(BaseModel): # pragma: no cov
239
241
  default=None, description="Environment (dev, staging, prod)."
240
242
  )
241
243
 
242
- # System context
244
+ # NOTE: System context
243
245
  hostname: Optional[str] = Field(
244
246
  default=None, description="Hostname where workflow is running."
245
247
  )
@@ -253,7 +255,7 @@ class Metadata(BaseModel): # pragma: no cov
253
255
  default=None, description="Workflow package version."
254
256
  )
255
257
 
256
- # Custom metadata
258
+ # NOTE: Custom metadata
257
259
  tags: Optional[list[str]] = Field(
258
260
  default_factory=list, description="Custom tags for categorization."
259
261
  )
@@ -320,6 +322,8 @@ class Metadata(BaseModel): # pragma: no cov
320
322
  import socket
321
323
  import sys
322
324
 
325
+ from .__about__ import __version__
326
+
323
327
  frame: Optional[FrameType] = currentframe()
324
328
  if frame is None:
325
329
  raise ValueError("Cannot get current frame")
@@ -384,7 +388,7 @@ class Metadata(BaseModel): # pragma: no cov
384
388
  hostname=hostname,
385
389
  ip_address=ip_address,
386
390
  python_version=python_version,
387
- package_version=extras_data.get("package_version"),
391
+ package_version=__version__,
388
392
  # NOTE: Custom metadata
389
393
  tags=extras_data.get("tags", []),
390
394
  metadata=extras_data.get("metadata", {}),
@@ -2046,7 +2050,7 @@ def get_trace(
2046
2050
  Args:
2047
2051
  run_id (str): A running ID.
2048
2052
  parent_run_id (str | None, default None): A parent running ID.
2049
- handlers:
2053
+ handlers (list):
2050
2054
  extras: An extra parameter that want to override the core
2051
2055
  config values.
2052
2056
  auto_pre_process (bool, default False)
@@ -2057,7 +2061,7 @@ def get_trace(
2057
2061
  handlers: list[DictData] = dynamic(
2058
2062
  "trace_handlers", f=handlers, extras=extras
2059
2063
  )
2060
- trace = Trace.model_validate(
2064
+ trace: Trace = Trace.model_validate(
2061
2065
  {
2062
2066
  "run_id": run_id,
2063
2067
  "parent_run_id": parent_run_id,
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
 
@@ -253,6 +233,33 @@ def gen_id(
253
233
  ).hexdigest()
254
234
 
255
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
+
256
263
  def default_gen_id() -> str:
257
264
  """Return running ID for making default ID for the Result model.
258
265
 
@@ -327,6 +334,8 @@ def cut_id(run_id: str, *, num: int = 8) -> str:
327
334
  Example:
328
335
  >>> cut_id(run_id='20240101081330000000T1354680202')
329
336
  '202401010813680202'
337
+ >>> cut_id(run_id='20240101081330000000T1354680202')
338
+ '54680202'
330
339
 
331
340
  Args:
332
341
  run_id: A running ID to cut.
@@ -394,3 +403,8 @@ def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
394
403
  else:
395
404
  obj_type: str = obj.__class__.__name__
396
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_")}
@@ -37,6 +37,7 @@ from threading import Event as ThreadEvent
37
37
  from typing import Any, Literal, Optional, Union
38
38
 
39
39
  from pydantic import BaseModel, Field
40
+ from pydantic.functional_serializers import field_serializer
40
41
  from pydantic.functional_validators import field_validator, model_validator
41
42
  from typing_extensions import Self
42
43
 
@@ -62,10 +63,10 @@ from .result import (
62
63
  from .reusables import has_template, param2template
63
64
  from .traces import Trace, get_trace
64
65
  from .utils import (
65
- UTC,
66
+ extract_id,
66
67
  gen_id,
67
68
  get_dt_now,
68
- replace_sec,
69
+ remove_sys_extras,
69
70
  )
70
71
 
71
72
 
@@ -243,8 +244,15 @@ class Workflow(BaseModel):
243
244
  f"{self.name!r}."
244
245
  )
245
246
 
247
+ # NOTE: Force update internal extras for handler circle execution.
248
+ self.extras.update({"__sys_break_circle_exec": self.name})
249
+
246
250
  return self
247
251
 
252
+ @field_serializer("extras")
253
+ def __serialize_extras(self, extras: DictData) -> DictData:
254
+ return remove_sys_extras(extras)
255
+
248
256
  def detail(self) -> DictData: # pragma: no cov
249
257
  """Return the detail of this workflow for generate markdown."""
250
258
  return self.model_dump(by_alias=True)
@@ -257,7 +265,8 @@ class Workflow(BaseModel):
257
265
  """
258
266
 
259
267
  def align_newline(value: str) -> str:
260
- return value.rstrip("\n").replace("\n", "\n ")
268
+ space: str = " " * 16
269
+ return value.rstrip("\n").replace("\n", f"\n{space}")
261
270
 
262
271
  info: str = (
263
272
  f"| Author: {author or 'nobody'} "
@@ -284,8 +293,7 @@ class Workflow(BaseModel):
284
293
  {align_newline(self.desc)}\n
285
294
  ## Parameters\n
286
295
  | name | type | default | description |
287
- | --- | --- | --- | : --- : |
288
-
296
+ | --- | --- | --- | : --- : |\n\n
289
297
  ## Jobs\n
290
298
  {align_newline(jobs)}
291
299
  """.lstrip(
@@ -314,8 +322,7 @@ class Workflow(BaseModel):
314
322
  f"{self.name!r}"
315
323
  )
316
324
  job: Job = self.jobs[name]
317
- if self.extras:
318
- job.extras = self.extras
325
+ job.extras = self.extras
319
326
  return job
320
327
 
321
328
  def parameterize(self, params: DictData) -> DictData:
@@ -334,8 +341,8 @@ class Workflow(BaseModel):
334
341
  execute method.
335
342
 
336
343
  Returns:
337
- DictData: The parameter value that validate with its parameter fields and
338
- 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.
339
346
 
340
347
  Raises:
341
348
  WorkflowError: If parameter value that want to validate does
@@ -366,33 +373,6 @@ class Workflow(BaseModel):
366
373
  "jobs": {},
367
374
  }
368
375
 
369
- def validate_release(self, dt: datetime) -> datetime:
370
- """Validate the release datetime that should was replaced second and
371
- millisecond to 0 and replaced timezone to None before checking it match
372
- with the set `on` field.
373
-
374
- Args:
375
- dt (datetime): A datetime object that want to validate.
376
-
377
- Returns:
378
- datetime: The validated release datetime.
379
- """
380
- if dt.tzinfo is None:
381
- dt = dt.replace(tzinfo=UTC)
382
-
383
- release: datetime = replace_sec(dt.astimezone(UTC))
384
-
385
- # NOTE: Return itself if schedule event does not set.
386
- if not self.on.schedule:
387
- return release
388
-
389
- for on in self.on.schedule:
390
- if release == on.cronjob.schedule(release, tz=UTC).next:
391
- return release
392
- raise WorkflowError(
393
- "Release datetime does not support for this workflow"
394
- )
395
-
396
376
  def release(
397
377
  self,
398
378
  release: datetime,
@@ -422,11 +402,12 @@ class Workflow(BaseModel):
422
402
 
423
403
  Args:
424
404
  release (datetime): A release datetime.
425
- params: A workflow parameter that pass to execute method.
426
- release_type:
405
+ params (DictData): A workflow parameter that pass to execute method.
406
+ release_type (ReleaseType): A release type that want to execute.
427
407
  run_id: (str) A workflow running ID.
428
408
  runs_metadata: (DictData)
429
- 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.
430
411
  override_log_name: (str) An override logging name that use
431
412
  instead the workflow name.
432
413
  timeout: (int) A workflow execution time out in second unit.
@@ -441,13 +422,9 @@ class Workflow(BaseModel):
441
422
  audit: Audit = audit or get_audit(extras=self.extras)
442
423
 
443
424
  # NOTE: Generate the parent running ID with not None value.
444
- if run_id:
445
- parent_run_id: str = run_id
446
- run_id: str = gen_id(name, unique=True)
447
- else:
448
- run_id: str = gen_id(name, unique=True)
449
- parent_run_id: str = run_id
450
-
425
+ parent_run_id, run_id = extract_id(
426
+ name, run_id=run_id, extras=self.extras
427
+ )
451
428
  context: DictData = {"status": WAIT}
452
429
  audit_data: DictData = {
453
430
  "name": name,
@@ -460,7 +437,7 @@ class Workflow(BaseModel):
460
437
  trace: Trace = get_trace(
461
438
  run_id, parent_run_id=parent_run_id, extras=self.extras
462
439
  )
463
- release: datetime = self.validate_release(dt=release)
440
+ release: datetime = self.on.validate_dt(dt=release)
464
441
  trace.info(f"[RELEASE]: Start {name!r} : {release:%Y-%m-%d %H:%M:%S}")
465
442
  values: DictData = param2template(
466
443
  params,
@@ -536,7 +513,7 @@ class Workflow(BaseModel):
536
513
  **(context["errors"] if "errors" in context else {}),
537
514
  },
538
515
  ),
539
- extras=self.extras,
516
+ extras=remove_sys_extras(self.extras),
540
517
  )
541
518
 
542
519
  def execute_job(
@@ -684,8 +661,9 @@ class Workflow(BaseModel):
684
661
  :rtype: Result
685
662
  """
686
663
  ts: float = time.monotonic()
687
- parent_run_id: Optional[str] = run_id
688
- run_id: str = gen_id(self.name, unique=True, extras=self.extras)
664
+ parent_run_id, run_id = extract_id(
665
+ self.name, run_id=run_id, extras=self.extras
666
+ )
689
667
  trace: Trace = get_trace(
690
668
  run_id, parent_run_id=parent_run_id, extras=self.extras
691
669
  )
@@ -913,6 +891,10 @@ class Workflow(BaseModel):
913
891
  ) -> Result: # pragma: no cov
914
892
  """Re-Execute workflow with passing the error context data.
915
893
 
894
+ Warnings:
895
+ This rerun method allow to rerun job execution level only. That mean
896
+ it does not support rerun only stage.
897
+
916
898
  Args:
917
899
  context: A context result that get the failed status.
918
900
  run_id: (Optional[str]) A workflow running ID.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.82
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
@@ -1,20 +1,20 @@
1
- ddeutil/workflow/__about__.py,sha256=GJmjvBMMhA0y8IwyPpPJLqLsuBWa6J90Pleir68QW5I,60
2
- ddeutil/workflow/__cron.py,sha256=avOagaHl9xXOmizeRWm13cOrty9Tw0vRjFq-xoEgpAY,29167
3
- ddeutil/workflow/__init__.py,sha256=elWSX2JPbjORH-CJIH_zR_nrdd8Xw95NjLK49R4Kqdg,3434
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
4
  ddeutil/workflow/__main__.py,sha256=Nqk5aO-HsZVKV2BmuJYeJEufJluipvCD9R1k2kMoJ3Y,8581
5
- ddeutil/workflow/__types.py,sha256=tA2vsr6mzTSzbWB1sb62c5GgxODlfVRz6FvgLNJtQao,4788
6
- ddeutil/workflow/audits.py,sha256=YPnWQYvhILPwPS6RhYKK0OG3lCeYFLuUNBxJvMlXf5w,26109
5
+ ddeutil/workflow/__types.py,sha256=IOKuJCxTUPHh8Z2JoLu_K7a85oq0VOcKBhpabiJ6qEE,5001
6
+ ddeutil/workflow/audits.py,sha256=H8yuMzXs_QAAKNox-HXdojk9CcilHYYQtklJYetoZv8,26955
7
7
  ddeutil/workflow/conf.py,sha256=VfPmwaBYEgOj8bu4eim13ayZwJ4Liy7I702aQf7vS8g,17644
8
- ddeutil/workflow/errors.py,sha256=Rqtuf1MGxA-hKGP5wMAkaeeayst-u4P2dX6Fp_pzbsA,5678
9
- ddeutil/workflow/event.py,sha256=qm7QHw-Pozm6oIUzAIxpDkPzzVZVtHgJIUlIle0vEfQ,13943
10
- ddeutil/workflow/job.py,sha256=WHWOVz0ErOUfbN_aqpDeNmvBvpbKhcFlzcvJmlCpJuI,48430
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
11
  ddeutil/workflow/params.py,sha256=y9f6DEIyae1j4awbj3Kbeq75-U2UPFlKv9K57Hdo_Go,17188
12
- ddeutil/workflow/result.py,sha256=3Lpyv2Jn6T1Uc-lRbweDucSCoBr0ZByHjffKj14bj6s,9492
12
+ ddeutil/workflow/result.py,sha256=0W3z5wAs3Dyr8r2vRMY5hl1MkvdsyXWJmQD4NmsDDOM,10194
13
13
  ddeutil/workflow/reusables.py,sha256=SBLJSxR8ELoWJErBfSMZS3Rr1O_93T-fFBpfn2AvxuA,25007
14
- ddeutil/workflow/stages.py,sha256=CCR_D6yqVo74PuMxfqhi8GeeAq8sRbyxReHob6yxrjI,123708
15
- ddeutil/workflow/traces.py,sha256=YN4XuRfQK523cNy8EVgz2iPh6s6WB865K9JezCNdM7E,74637
16
- ddeutil/workflow/utils.py,sha256=vQwFu-wPK-lDiX2L8AZIahCkKEF6I0MCrZ1LlP8xkoQ,12011
17
- ddeutil/workflow/workflow.py,sha256=VN8i0mVyuCUw1kk_CqLkN-8dWLON9i4vTLbHp6lyS-s,42961
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
18
  ddeutil/workflow/api/__init__.py,sha256=5DzYL3ngceoRshh5HYCSVWChqNJSiP01E1bEd8XxPi0,4799
19
19
  ddeutil/workflow/api/log_conf.py,sha256=WfS3udDLSyrP-C80lWOvxxmhd_XWKvQPkwDqKblcH3E,1834
20
20
  ddeutil/workflow/api/routes/__init__.py,sha256=JRaJZB0D6mgR17MbZo8yLtdYDtD62AA8MdKlFqhG84M,420
@@ -27,9 +27,9 @@ ddeutil/workflow/plugins/providers/aws.py,sha256=61uIFBEWt-_D5Sui24qUPier1Hiqlw_
27
27
  ddeutil/workflow/plugins/providers/az.py,sha256=o3dh011lEtmr7-d7FPZJPgXdT0ytFzKfc5xnVxSyXGU,34867
28
28
  ddeutil/workflow/plugins/providers/container.py,sha256=DSN0RWxMjTJN5ANheeMauDaPa3X6Z2E1eGUcctYkENw,22134
29
29
  ddeutil/workflow/plugins/providers/gcs.py,sha256=KgAOdMBvdbMLTH_z_FwVriBFtZfKEYx8_34jzUOVjTY,27460
30
- ddeutil_workflow-0.0.82.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
31
- ddeutil_workflow-0.0.82.dist-info/METADATA,sha256=lB4PBeyneqHLwG0S4Lt_Xz8eCz4Lxn2bkDDHIFcBpxE,16087
32
- ddeutil_workflow-0.0.82.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- ddeutil_workflow-0.0.82.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
34
- ddeutil_workflow-0.0.82.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
35
- ddeutil_workflow-0.0.82.dist-info/RECORD,,
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,,