ddeutil-workflow 0.0.27__py3-none-any.whl → 0.0.28__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.27"
1
+ __version__: str = "0.0.28"
@@ -5,25 +5,21 @@
5
5
  # ------------------------------------------------------------------------------
6
6
  from __future__ import annotations
7
7
 
8
- import asyncio
9
8
  import contextlib
10
- import uuid
11
9
  from collections.abc import AsyncIterator
12
10
  from datetime import datetime, timedelta
13
- from queue import Empty, Queue
14
11
  from typing import TypedDict
15
12
 
16
13
  from dotenv import load_dotenv
17
14
  from fastapi import FastAPI
18
15
  from fastapi.middleware.gzip import GZipMiddleware
19
16
  from fastapi.responses import UJSONResponse
20
- from pydantic import BaseModel
21
17
 
22
18
  from ..__about__ import __version__
23
19
  from ..conf import config, get_logger
24
20
  from ..scheduler import ReleaseThread, ReleaseThreads
25
21
  from ..workflow import WorkflowQueue, WorkflowTask
26
- from .repeat import repeat_at, repeat_every
22
+ from .repeat import repeat_at
27
23
 
28
24
  load_dotenv()
29
25
  logger = get_logger("ddeutil.workflow")
@@ -32,11 +28,6 @@ logger = get_logger("ddeutil.workflow")
32
28
  class State(TypedDict):
33
29
  """TypeDict for State of FastAPI application."""
34
30
 
35
- # NOTE: For upper queue route.
36
- upper_queue: Queue
37
- upper_result: dict[str, str]
38
-
39
- # NOTE: For schedule listener.
40
31
  scheduler: list[str]
41
32
  workflow_threads: ReleaseThreads
42
33
  workflow_tasks: list[WorkflowTask]
@@ -46,15 +37,11 @@ class State(TypedDict):
46
37
  @contextlib.asynccontextmanager
47
38
  async def lifespan(a: FastAPI) -> AsyncIterator[State]:
48
39
  """Lifespan function for the FastAPI application."""
49
- a.state.upper_queue = Queue()
50
- a.state.upper_result = {}
51
40
  a.state.scheduler = []
52
41
  a.state.workflow_threads = {}
53
42
  a.state.workflow_tasks = []
54
43
  a.state.workflow_queue = {}
55
44
 
56
- await asyncio.create_task(broker_upper_messages())
57
-
58
45
  yield {
59
46
  "upper_queue": a.state.upper_queue,
60
47
  "upper_result": a.state.upper_result,
@@ -87,50 +74,11 @@ app = FastAPI(
87
74
  app.add_middleware(GZipMiddleware, minimum_size=1000)
88
75
 
89
76
 
90
- @repeat_every(seconds=10)
91
- async def broker_upper_messages():
92
- """Broker for receive message from the `/upper` path and change it to upper
93
- case. This broker use interval running in background every 10 seconds.
94
- """
95
- for _ in range(10):
96
- try:
97
- obj = app.state.upper_queue.get_nowait()
98
- app.state.upper_result[obj["request_id"]] = obj["text"].upper()
99
- logger.info(f"Upper message: {app.state.upper_result}")
100
- except Empty:
101
- pass
102
- await asyncio.sleep(0.0001)
103
-
104
-
105
- class Payload(BaseModel):
106
- text: str
107
-
108
-
109
- async def get_result(request_id: str) -> dict[str, str]:
110
- """Get data from output dict that global."""
111
- while True:
112
- if request_id in app.state.upper_result:
113
- result: str = app.state.upper_result[request_id]
114
- del app.state.upper_result[request_id]
115
- return {"message": result}
116
- await asyncio.sleep(0.0025)
117
-
118
-
119
77
  @app.get("/")
120
78
  async def health():
121
79
  return {"message": "Workflow API already start up"}
122
80
 
123
81
 
124
- @app.post(f"{config.prefix_path}/upper")
125
- async def message_upper(payload: Payload):
126
- """Convert message from any case to the upper case."""
127
- request_id: str = str(uuid.uuid4())
128
- app.state.upper_queue.put(
129
- {"text": payload.text, "request_id": request_id},
130
- )
131
- return await get_result(request_id)
132
-
133
-
134
82
  # NOTE: Enable the workflow route.
135
83
  if config.enable_route_workflow:
136
84
  from .route import workflow_route
ddeutil/workflow/conf.py CHANGED
@@ -24,9 +24,11 @@ from typing_extensions import Self
24
24
 
25
25
  from .__types import DictData, TupleStr
26
26
 
27
+ PREFIX: str = "WORKFLOW"
28
+
27
29
 
28
30
  def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
29
- return os.getenv(f"WORKFLOW_{var}", default)
31
+ return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
30
32
 
31
33
 
32
34
  def glob_files(path: Path) -> Iterator[Path]: # pragma: no cov
@@ -80,14 +82,18 @@ def get_logger(name: str):
80
82
 
81
83
 
82
84
  class Config: # pragma: no cov
83
- """Config object for keeping application configuration on current session
85
+ """Config object for keeping core configurations on the current session
84
86
  without changing when if the application still running.
87
+
88
+ The config value can change when you call that config property again.
85
89
  """
86
90
 
91
+ __slots__ = ()
92
+
87
93
  # NOTE: Core
88
94
  @property
89
95
  def root_path(self) -> Path:
90
- return Path(env("ROOT_PATH", "."))
96
+ return Path(env("CORE_ROOT_PATH", "."))
91
97
 
92
98
  @property
93
99
  def conf_path(self) -> Path:
@@ -95,7 +101,7 @@ class Config: # pragma: no cov
95
101
 
96
102
  :rtype: Path
97
103
  """
98
- return self.root_path / env("CORE_PATH_CONF", "conf")
104
+ return self.root_path / env("CORE_CONF_PATH", "conf")
99
105
 
100
106
  @property
101
107
  def tz(self) -> ZoneInfo:
@@ -349,7 +355,7 @@ class Loader(SimLoad):
349
355
  class BaseLog(BaseModel, ABC):
350
356
  """Base Log Pydantic Model with abstraction class property that implement
351
357
  only model fields. This model should to use with inherit to logging
352
- sub-class like file, sqlite, etc.
358
+ subclass like file, sqlite, etc.
353
359
  """
354
360
 
355
361
  name: str = Field(description="A workflow name.")
@@ -357,9 +363,7 @@ class BaseLog(BaseModel, ABC):
357
363
  type: str = Field(description="A running type before logging.")
358
364
  context: DictData = Field(
359
365
  default_factory=dict,
360
- description=(
361
- "A context data that receive from a workflow execution result.",
362
- ),
366
+ description="A context that receive from a workflow execution result.",
363
367
  )
364
368
  parent_run_id: Optional[str] = Field(default=None)
365
369
  run_id: str
@@ -386,7 +390,7 @@ class BaseLog(BaseModel, ABC):
386
390
 
387
391
  class FileLog(BaseLog):
388
392
  """File Log Pydantic Model that use to saving log data from result of
389
- workflow execution. It inherit from BaseLog model that implement the
393
+ workflow execution. It inherits from BaseLog model that implement the
390
394
  ``self.save`` method for file.
391
395
  """
392
396
 
@@ -526,7 +530,7 @@ class SQLiteLog(BaseLog): # pragma: no cov
526
530
  primary key ( run_id )
527
531
  """
528
532
 
529
- def save(self, excluded: list[str] | None) -> None:
533
+ def save(self, excluded: list[str] | None) -> SQLiteLog:
530
534
  """Save logging data that receive a context data from a workflow
531
535
  execution result.
532
536
  """
@@ -549,7 +553,7 @@ Log = Union[
549
553
  ]
550
554
 
551
555
 
552
- def get_log() -> Log: # pragma: no cov
556
+ def get_log() -> type[Log]: # pragma: no cov
553
557
  if config.log_path.is_file():
554
558
  return SQLiteLog
555
559
  return FileLog
ddeutil/workflow/cron.py CHANGED
@@ -32,10 +32,10 @@ def interval2crontab(
32
32
  ) -> str:
33
33
  """Return the crontab string that was generated from specific values.
34
34
 
35
- :param interval: A interval value that is one of 'daily', 'weekly', or
35
+ :param interval: An interval value that is one of 'daily', 'weekly', or
36
36
  'monthly'.
37
37
  :param day: A day value that will be day of week. The default value is
38
- monday if it be weekly interval.
38
+ monday if it is weekly interval.
39
39
  :param time: A time value that passing with format '%H:%M'.
40
40
 
41
41
  Examples:
@@ -50,9 +50,9 @@ def interval2crontab(
50
50
  """
51
51
  d: str = "*"
52
52
  if interval == "weekly":
53
- d = WEEKDAYS[(day or "monday")[:3].title()]
53
+ d = str(WEEKDAYS[(day or "monday")[:3].title()])
54
54
  elif interval == "monthly" and day:
55
- d = WEEKDAYS[day[:3].title()]
55
+ d = str(WEEKDAYS[day[:3].title()])
56
56
 
57
57
  h, m = tuple(
58
58
  i.lstrip("0") if i != "00" else "0" for i in time.split(":", maxsplit=1)
@@ -95,7 +95,7 @@ class On(BaseModel):
95
95
 
96
96
  :param value: A mapping value that will generate crontab before create
97
97
  schedule model.
98
- :param externals: A extras external parameter that will keep in extras.
98
+ :param externals: An extras external parameter that will keep in extras.
99
99
  """
100
100
  passing: DictStr = {}
101
101
  if "timezone" in value:
@@ -114,8 +114,8 @@ class On(BaseModel):
114
114
  """Constructor from the name of config that will use loader object for
115
115
  getting the data.
116
116
 
117
- :param name: A name of config that will getting from loader.
118
- :param externals: A extras external parameter that will keep in extras.
117
+ :param name: A name of config that will get from loader.
118
+ :param externals: An extras external parameter that will keep in extras.
119
119
  """
120
120
  externals: DictData = externals or {}
121
121
  loader: Loader = Loader(name, externals=externals)
@@ -4,7 +4,7 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  """Exception objects for this package do not do anything because I want to
7
- create the lightweight workflow package. So, this module do just a exception
7
+ create the lightweight workflow package. So, this module do just an exception
8
8
  annotate for handle error only.
9
9
  """
10
10
  from __future__ import annotations
ddeutil/workflow/hook.py CHANGED
@@ -87,9 +87,12 @@ def make_registry(submodule: str) -> dict[str, Registry]:
87
87
  for fstr, func in inspect.getmembers(importer, inspect.isfunction):
88
88
  # NOTE: check function attribute that already set tag by
89
89
  # ``utils.tag`` decorator.
90
- if not hasattr(func, "tag"):
90
+ if not (hasattr(func, "tag") and hasattr(func, "name")):
91
91
  continue
92
92
 
93
+ # NOTE: Define type of the func value.
94
+ func: TagFunc
95
+
93
96
  # NOTE: Create new register name if it not exists
94
97
  if func.name not in rs:
95
98
  rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
@@ -124,9 +127,21 @@ def extract_hook(hook: str) -> Callable[[], TagFunc]:
124
127
  :raise NotImplementedError: When the searching hook's function result does
125
128
  not exist in the registry.
126
129
  :raise NotImplementedError: When the searching hook's tag result does not
127
- exists in the registry with its function key.
130
+ exist in the registry with its function key.
128
131
 
129
132
  :param hook: A hook value that able to match with Task regex.
133
+
134
+ The format of hook value should contain 3 regular expression groups
135
+ which match with the below config format:
136
+
137
+ >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
138
+
139
+ Examples:
140
+ >>> extract_hook("tasks/el-postgres-to-delta@polars")
141
+ ...
142
+ >>> extract_hook("tasks/return-type-not-valid@raise")
143
+ ...
144
+
130
145
  :rtype: Callable[[], TagFunc]
131
146
  """
132
147
  if not (found := Re.RE_TASK_FMT.search(hook)):
ddeutil/workflow/job.py CHANGED
@@ -83,7 +83,7 @@ def make(
83
83
  if len(matrix) == 0:
84
84
  return [{}]
85
85
 
86
- # NOTE: Remove matrix that exists on the exclude.
86
+ # NOTE: Remove matrix that exists on the excluded.
87
87
  final: list[DictStr] = []
88
88
  for r in cross_product(matrix=matrix):
89
89
  if any(
@@ -101,7 +101,7 @@ def make(
101
101
  add: list[DictStr] = []
102
102
  for inc in include:
103
103
  # VALIDATE:
104
- # Validate any key in include list should be a subset of some one
104
+ # Validate any key in include list should be a subset of someone
105
105
  # in matrix.
106
106
  if all(not (set(inc.keys()) <= set(m.keys())) for m in final):
107
107
  raise ValueError(
@@ -128,9 +128,9 @@ class Strategy(BaseModel):
128
128
  special job with combination of matrix data.
129
129
 
130
130
  This model does not be the part of job only because you can use it to
131
- any model object. The propose of this model is generate metrix result that
132
- comming from combination logic with any matrix values for running it with
133
- parallelism.
131
+ any model object. The objective of this model is generating metrix result
132
+ that comming from combination logic with any matrix values for running it
133
+ with parallelism.
134
134
 
135
135
  [1, 2, 3] x [a, b] --> [1a], [1b], [2a], [2b], [3a], [3b]
136
136
 
@@ -180,7 +180,7 @@ class Strategy(BaseModel):
180
180
  """Rename key that use dash to underscore because Python does not
181
181
  support this character exist in any variable name.
182
182
 
183
- :param values: A parsing values to this models
183
+ :param values: A parsing values to these models
184
184
  :rtype: DictData
185
185
  """
186
186
  dash2underscore("max-parallel", values)
@@ -226,7 +226,7 @@ class Job(BaseModel):
226
226
  """Job Pydantic model object (short descripte: a group of stages).
227
227
 
228
228
  This job model allow you to use for-loop that call matrix strategy. If
229
- you pass matrix mapping and it able to generate, you will see it running
229
+ you pass matrix mapping, and it is able to generate, you will see it running
230
230
  with loop of matrix values.
231
231
 
232
232
  Data Validate:
@@ -355,7 +355,7 @@ class Job(BaseModel):
355
355
  return all(need in jobs for need in self.needs)
356
356
 
357
357
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
358
- """Set an outputs from execution process to the receive context. The
358
+ """Set an outputs from execution process to the received context. The
359
359
  result from execution will pass to value of ``strategies`` key.
360
360
 
361
361
  For example of setting output method, If you receive execute output
@@ -420,7 +420,7 @@ class Job(BaseModel):
420
420
  strategy and return with context of this strategy data.
421
421
 
422
422
  The result of this execution will return result with strategy ID
423
- that generated from the `gen_id` function with a input strategy value.
423
+ that generated from the `gen_id` function with an input strategy value.
424
424
 
425
425
  :raise JobException: If it has any error from ``StageException`` or
426
426
  ``UtilException``.
@@ -429,7 +429,7 @@ class Job(BaseModel):
429
429
  This value will pass to the `matrix` key for templating.
430
430
  :param params: A dynamic parameters that will deepcopy to the context.
431
431
  :param run_id: A job running ID for this strategy execution.
432
- :param event: An manger event that pass to the PoolThreadExecutor.
432
+ :param event: An event manager that pass to the PoolThreadExecutor.
433
433
 
434
434
  :rtype: Result
435
435
  """
@@ -496,7 +496,7 @@ class Job(BaseModel):
496
496
  # PARAGRAPH:
497
497
  #
498
498
  # I do not use below syntax because `params` dict be the
499
- # reference memory pointer and it was changed when I action
499
+ # reference memory pointer, and it was changed when I action
500
500
  # anything like update or re-construct this.
501
501
  #
502
502
  # ... params |= stage.execute(params=params)
@@ -513,7 +513,9 @@ class Job(BaseModel):
513
513
  #
514
514
  try:
515
515
  stage.set_outputs(
516
- stage.execute(params=context, run_id=run_id).context,
516
+ stage.handler_execute(
517
+ params=context, run_id=run_id
518
+ ).context,
517
519
  to=context,
518
520
  )
519
521
  except (StageException, UtilException) as err:
@@ -566,7 +568,7 @@ class Job(BaseModel):
566
568
  run_id: str = run_id or gen_id(self.id or "", unique=True)
567
569
  context: DictData = {}
568
570
 
569
- # NOTE: Normal Job execution without parallel strategy matrix. It use
571
+ # NOTE: Normal Job execution without parallel strategy matrix. It uses
570
572
  # for-loop to control strategy execution sequentially.
571
573
  if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
572
574
  for strategy in self.strategy.make():
@@ -585,8 +587,7 @@ class Job(BaseModel):
585
587
  event: Event = Event()
586
588
 
587
589
  # IMPORTANT: Start running strategy execution by multithreading because
588
- # it will running by strategy values without waiting previous
589
- # execution.
590
+ # it will run by strategy values without waiting previous execution.
590
591
  with ThreadPoolExecutor(
591
592
  max_workers=self.strategy.max_parallel,
592
593
  thread_name_prefix="job_strategy_exec_",
@@ -618,7 +619,7 @@ class Job(BaseModel):
618
619
  timeout: int = 1800,
619
620
  ) -> Result:
620
621
  """Job parallel pool futures catching with fail-fast mode. That will
621
- stop and set event on all not done futures if it receive the first
622
+ stop and set event on all not done futures if it receives the first
622
623
  exception from all running futures.
623
624
 
624
625
  :param event: An event manager instance that able to set stopper on the
@@ -75,7 +75,7 @@ class DatetimeParam(DefaultParam):
75
75
  default: datetime = Field(default_factory=get_dt_now)
76
76
 
77
77
  def receive(self, value: str | datetime | date | None = None) -> datetime:
78
- """Receive value that match with datetime. If a input value pass with
78
+ """Receive value that match with datetime. If an input value pass with
79
79
  None, it will use default value instead.
80
80
 
81
81
  :param value: A value that want to validate with datetime parameter
@@ -98,7 +98,7 @@ class DatetimeParam(DefaultParam):
98
98
  return datetime.fromisoformat(value)
99
99
  except ValueError:
100
100
  raise ParamValueException(
101
- f"Invalid isoformat string: {value!r}"
101
+ f"Invalid the ISO format string: {value!r}"
102
102
  ) from None
103
103
 
104
104
 
@@ -158,7 +158,7 @@ class ChoiceParam(BaseParam):
158
158
  :rtype: str
159
159
  """
160
160
  # NOTE:
161
- # Return the first value in options if does not pass any input value
161
+ # Return the first value in options if it does not pass any input value
162
162
  if value is None:
163
163
  return self.options[0]
164
164
  if value not in self.options:
@@ -37,8 +37,8 @@ class Result:
37
37
 
38
38
  @model_validator(mode="after")
39
39
  def __prepare_run_id(self) -> Self:
40
- """Prepare running ID which use default ID if it initialize at the first
41
- time
40
+ """Prepare running ID which use default ID if it initializes at the
41
+ first time.
42
42
 
43
43
  :rtype: Self
44
44
  """
@@ -84,7 +84,7 @@ class Result:
84
84
 
85
85
  def receive_jobs(self, result: Result) -> Self:
86
86
  """Receive context from another result object that use on the workflow
87
- execution which create a ``jobs`` keys on the context if it do not
87
+ execution which create a ``jobs`` keys on the context if it does not
88
88
  exist.
89
89
 
90
90
  :rtype: Self
@@ -149,7 +149,7 @@ class WorkflowSchedule(BaseModel):
149
149
  @field_validator("on", mode="after")
150
150
  def __on_no_dup__(cls, value: list[On]) -> list[On]:
151
151
  """Validate the on fields should not contain duplicate values and if it
152
- contain every minute value, it should has only one on value.
152
+ contains every minute value, it should have only one on value.
153
153
 
154
154
  :rtype: list[On]
155
155
  """
@@ -195,8 +195,8 @@ class WorkflowSchedule(BaseModel):
195
195
  wf: Workflow = Workflow.from_loader(self.name, externals=extras)
196
196
  wf_queue: WorkflowQueue = queue[self.alias]
197
197
 
198
- # IMPORTANT: Create the default 'on' value if it does not passing
199
- # the on field to the Schedule object.
198
+ # IMPORTANT: Create the default 'on' value if it does not pass the `on`
199
+ # field to the Schedule object.
200
200
  ons: list[On] = self.on or wf.on.copy()
201
201
 
202
202
  for on in ons:
@@ -223,7 +223,7 @@ class WorkflowSchedule(BaseModel):
223
223
  class Schedule(BaseModel):
224
224
  """Schedule Pydantic model that use to run with any scheduler package.
225
225
 
226
- It does not equal the on value in Workflow model but it use same logic
226
+ It does not equal the on value in Workflow model, but it uses same logic
227
227
  to running release date with crontab interval.
228
228
  """
229
229
 
@@ -368,7 +368,7 @@ def schedule_task(
368
368
  :param stop: A stop datetime object that force stop running scheduler.
369
369
  :param queue: A mapping of alias name and WorkflowQueue object.
370
370
  :param threads: A mapping of alias name and Thread object.
371
- :param log: A log class that want to making log object.
371
+ :param log: A log class that want to make log object.
372
372
 
373
373
  :rtype: CancelJob | None
374
374
  """
@@ -449,7 +449,7 @@ def schedule_task(
449
449
 
450
450
 
451
451
  def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
452
- """Monitoring function that running every five minute for track long running
452
+ """Monitoring function that running every five minute for track long-running
453
453
  thread instance from the schedule_control function that run every minute.
454
454
 
455
455
  :param threads: A mapping of Thread object and its name.
@@ -479,7 +479,7 @@ def schedule_control(
479
479
  """Scheduler control function that running every minute.
480
480
 
481
481
  :param schedules: A list of workflow names that want to schedule running.
482
- :param stop: An datetime value that use to stop running schedule.
482
+ :param stop: A datetime value that use to stop running schedule.
483
483
  :param externals: An external parameters that pass to Loader.
484
484
  :param log:
485
485
 
@@ -554,7 +554,7 @@ def schedule_control(
554
554
  scheduler.run_pending()
555
555
  time.sleep(1)
556
556
 
557
- # NOTE: Break the scheduler when the control job does not exists.
557
+ # NOTE: Break the scheduler when the control job does not exist.
558
558
  if not scheduler.get_jobs("control"):
559
559
  scheduler.clear("monitor")
560
560
 
@@ -585,7 +585,7 @@ def schedule_runner(
585
585
 
586
586
  :param stop: A stop datetime object that force stop running scheduler.
587
587
  :param externals:
588
- :param excluded: A list of schedule name that want to excluded from finding.
588
+ :param excluded: A list of schedule name that want to exclude from finding.
589
589
 
590
590
  :rtype: list[str]
591
591