ddeutil-workflow 0.0.9__py3-none-any.whl → 0.0.10__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/stage.py CHANGED
@@ -19,7 +19,6 @@ from __future__ import annotations
19
19
 
20
20
  import contextlib
21
21
  import inspect
22
- import logging
23
22
  import os
24
23
  import subprocess
25
24
  import sys
@@ -46,6 +45,7 @@ from typing_extensions import Self
46
45
 
47
46
  from .__types import DictData, DictStr, Re, TupleStr
48
47
  from .exceptions import StageException
48
+ from .log import get_logger
49
49
  from .utils import (
50
50
  Registry,
51
51
  Result,
@@ -58,6 +58,9 @@ from .utils import (
58
58
  )
59
59
 
60
60
  P = ParamSpec("P")
61
+ logger = get_logger("ddeutil.workflow")
62
+
63
+
61
64
  __all__: TupleStr = (
62
65
  "Stage",
63
66
  "EmptyStage",
@@ -86,7 +89,7 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
86
89
  return func(self, *args, **kwargs).set_run_id(self.run_id)
87
90
  except Exception as err:
88
91
  # NOTE: Start catching error from the stage execution.
89
- logging.error(
92
+ logger.error(
90
93
  f"({self.run_id}) [STAGE]: {err.__class__.__name__}: {err}"
91
94
  )
92
95
  if str2bool(
@@ -141,6 +144,7 @@ class BaseStage(BaseModel, ABC):
141
144
  default=None,
142
145
  description="A running stage ID.",
143
146
  repr=False,
147
+ exclude=True,
144
148
  )
145
149
 
146
150
  @model_validator(mode="after")
@@ -191,7 +195,7 @@ class BaseStage(BaseModel, ABC):
191
195
  self.id
192
196
  or str2bool(os.getenv("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false"))
193
197
  ):
194
- logging.debug(
198
+ logger.debug(
195
199
  f"({self.run_id}) [STAGE]: Output does not set because this "
196
200
  f"stage does not set ID or default stage ID config flag not be "
197
201
  f"True."
@@ -208,7 +212,7 @@ class BaseStage(BaseModel, ABC):
208
212
  _id: str = gen_id(param2template(self.name, params=to))
209
213
 
210
214
  # NOTE: Set the output to that stage generated ID.
211
- logging.debug(
215
+ logger.debug(
212
216
  f"({self.run_id}) [STAGE]: Set output complete with stage ID: {_id}"
213
217
  )
214
218
  to["stages"][_id] = {"outputs": output}
@@ -231,7 +235,7 @@ class BaseStage(BaseModel, ABC):
231
235
  raise TypeError("Return type of condition does not be boolean")
232
236
  return not rs
233
237
  except Exception as err:
234
- logging.error(f"({self.run_id}) [STAGE]: {err}")
238
+ logger.error(f"({self.run_id}) [STAGE]: {err}")
235
239
  raise StageException(f"{err.__class__.__name__}: {err}") from err
236
240
 
237
241
 
@@ -258,7 +262,7 @@ class EmptyStage(BaseStage):
258
262
  :param params: A context data that want to add output result. But this
259
263
  stage does not pass any output.
260
264
  """
261
- logging.info(
265
+ logger.info(
262
266
  f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
263
267
  f"( {param2template(self.echo, params=params) or '...'} )"
264
268
  )
@@ -314,7 +318,7 @@ class BashStage(BaseStage):
314
318
  # NOTE: Make this .sh file able to executable.
315
319
  make_exec(f"./{f_name}")
316
320
 
317
- logging.debug(
321
+ logger.debug(
318
322
  f"({self.run_id}) [STAGE]: Start create `.sh` file and running a "
319
323
  f"bash statement."
320
324
  )
@@ -336,7 +340,7 @@ class BashStage(BaseStage):
336
340
  with self.__prepare_bash(
337
341
  bash=bash, env=param2template(self.env, params)
338
342
  ) as sh:
339
- logging.info(f"({self.run_id}) [STAGE]: Shell-Execute: {sh}")
343
+ logger.info(f"({self.run_id}) [STAGE]: Shell-Execute: {sh}")
340
344
  rs: CompletedProcess = subprocess.run(
341
345
  sh,
342
346
  shell=False,
@@ -424,7 +428,7 @@ class PyStage(BaseStage):
424
428
  _locals: DictData = {}
425
429
 
426
430
  # NOTE: Start exec the run statement.
427
- logging.info(f"({self.run_id}) [STAGE]: Py-Execute: {self.name}")
431
+ logger.info(f"({self.run_id}) [STAGE]: Py-Execute: {self.name}")
428
432
  exec(run, _globals, _locals)
429
433
 
430
434
  return Result(
@@ -531,7 +535,7 @@ class HookStage(BaseStage):
531
535
  if k.removeprefix("_") in args:
532
536
  args[k] = args.pop(k.removeprefix("_"))
533
537
 
534
- logging.info(
538
+ logger.info(
535
539
  f"({self.run_id}) [STAGE]: Hook-Execute: {t_func.name}@{t_func.tag}"
536
540
  )
537
541
  rs: DictData = t_func(**param2template(args, params))
@@ -583,7 +587,7 @@ class TriggerStage(BaseStage):
583
587
  pipe: Pipeline = Pipeline.from_loader(
584
588
  name=_trigger, externals={"run_id": self.run_id}
585
589
  )
586
- logging.info(f"({self.run_id}) [STAGE]: Trigger-Execute: {_trigger!r}")
590
+ logger.info(f"({self.run_id}) [STAGE]: Trigger-Execute: {_trigger!r}")
587
591
  return pipe.execute(params=param2template(self.params, params))
588
592
 
589
593
 
ddeutil/workflow/utils.py CHANGED
@@ -14,14 +14,14 @@ from abc import ABC, abstractmethod
14
14
  from ast import Call, Constant, Expr, Module, Name, parse
15
15
  from collections.abc import Iterator
16
16
  from datetime import date, datetime
17
- from functools import wraps
17
+ from functools import cached_property, wraps
18
18
  from hashlib import md5
19
19
  from importlib import import_module
20
20
  from inspect import isfunction
21
21
  from itertools import chain, islice, product
22
22
  from pathlib import Path
23
23
  from random import randrange
24
- from typing import Any, Callable, Literal, Optional, Protocol, Union
24
+ from typing import Any, Callable, Literal, Optional, Protocol, TypeVar, Union
25
25
  from zoneinfo import ZoneInfo
26
26
 
27
27
  try:
@@ -30,16 +30,20 @@ except ImportError:
30
30
  from typing_extensions import ParamSpec
31
31
 
32
32
  from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy, str2bool
33
- from ddeutil.io import PathData, search_env_replace
33
+ from ddeutil.io import PathData, PathSearch, YamlFlResolve, search_env_replace
34
34
  from ddeutil.io.models.lineage import dt_now
35
35
  from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
36
+ from pydantic.functional_serializers import field_serializer
36
37
  from pydantic.functional_validators import model_validator
37
38
  from typing_extensions import Self
38
39
 
39
40
  from .__types import DictData, Matrix, Re
40
41
  from .exceptions import ParamValueException, UtilException
41
42
 
43
+ logger = logging.getLogger("ddeutil.workflow")
42
44
  P = ParamSpec("P")
45
+ AnyModel = TypeVar("AnyModel", bound=BaseModel)
46
+ AnyModelType = type[AnyModel]
43
47
 
44
48
 
45
49
  def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
@@ -51,11 +55,13 @@ def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
51
55
  )
52
56
 
53
57
 
54
- def delay() -> None:
58
+ def delay(second: float = 0) -> None:
55
59
  """Delay time that use time.sleep with random second value between
56
60
  0.00 - 0.99 seconds.
61
+
62
+ :param second: A second number that want to adds-on random value.
57
63
  """
58
- time.sleep(randrange(0, 99, step=10) / 100)
64
+ time.sleep(second + randrange(0, 99, step=10) / 100)
59
65
 
60
66
 
61
67
  class Engine(BaseModel):
@@ -143,6 +149,112 @@ def config() -> ConfParams:
143
149
  )
144
150
 
145
151
 
152
+ class SimLoad:
153
+ """Simple Load Object that will search config data by given some identity
154
+ value like name of pipeline or on.
155
+
156
+ :param name: A name of config data that will read by Yaml Loader object.
157
+ :param params: A Params model object.
158
+ :param externals: An external parameters
159
+
160
+ Noted:
161
+ ---
162
+ The config data should have ``type`` key for modeling validation that
163
+ make this loader know what is config should to do pass to.
164
+
165
+ ... <identity-key>:
166
+ ... type: <importable-object>
167
+ ... <key-data>: <value-data>
168
+ ... ...
169
+
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ name: str,
175
+ params: ConfParams,
176
+ externals: DictData | None = None,
177
+ ) -> None:
178
+ self.data: DictData = {}
179
+ for file in PathSearch(params.engine.paths.conf).files:
180
+ if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
181
+ data := YamlFlResolve(file).read().get(name, {})
182
+ ):
183
+ self.data = data
184
+
185
+ # VALIDATE: check the data that reading should not empty.
186
+ if not self.data:
187
+ raise ValueError(f"Config {name!r} does not found on conf path")
188
+
189
+ self.conf_params: ConfParams = params
190
+ self.externals: DictData = externals or {}
191
+ self.data.update(self.externals)
192
+
193
+ @classmethod
194
+ def finds(
195
+ cls,
196
+ obj: object,
197
+ params: ConfParams,
198
+ *,
199
+ include: list[str] | None = None,
200
+ exclude: list[str] | None = None,
201
+ ) -> Iterator[tuple[str, DictData]]:
202
+ """Find all data that match with object type in config path. This class
203
+ method can use include and exclude list of identity name for filter and
204
+ adds-on.
205
+ """
206
+ exclude: list[str] = exclude or []
207
+ for file in PathSearch(params.engine.paths.conf).files:
208
+ if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
209
+ values := YamlFlResolve(file).read()
210
+ ):
211
+ for key, data in values.items():
212
+ if key in exclude:
213
+ continue
214
+ if issubclass(get_type(data["type"], params), obj) and (
215
+ include is None or all(i in data for i in include)
216
+ ):
217
+ yield key, data
218
+
219
+ @cached_property
220
+ def type(self) -> AnyModelType:
221
+ """Return object of string type which implement on any registry. The
222
+ object type.
223
+
224
+ :rtype: AnyModelType
225
+ """
226
+ if not (_typ := self.data.get("type")):
227
+ raise ValueError(
228
+ f"the 'type' value: {_typ} does not exists in config data."
229
+ )
230
+ return get_type(_typ, self.conf_params)
231
+
232
+
233
+ class Loader(SimLoad):
234
+ """Loader Object that get the config `yaml` file from current path.
235
+
236
+ :param name: A name of config data that will read by Yaml Loader object.
237
+ :param externals: An external parameters
238
+ """
239
+
240
+ @classmethod
241
+ def finds(
242
+ cls,
243
+ obj: object,
244
+ *,
245
+ include: list[str] | None = None,
246
+ exclude: list[str] | None = None,
247
+ **kwargs,
248
+ ) -> DictData:
249
+ """Override the find class method from the Simple Loader object."""
250
+ return super().finds(
251
+ obj=obj, params=config(), include=include, exclude=exclude
252
+ )
253
+
254
+ def __init__(self, name: str, externals: DictData) -> None:
255
+ super().__init__(name, config(), externals)
256
+
257
+
146
258
  def gen_id(
147
259
  value: Any,
148
260
  *,
@@ -176,6 +288,26 @@ def gen_id(
176
288
  ).hexdigest()
177
289
 
178
290
 
291
+ def get_type(t: str, params: ConfParams) -> AnyModelType:
292
+ """Return import type from string importable value in the type key.
293
+
294
+ :param t: A importable type string.
295
+ :param params: A config parameters that use registry to search this
296
+ type.
297
+ :rtype: AnyModelType
298
+ """
299
+ try:
300
+ # NOTE: Auto adding module prefix if it does not set
301
+ return import_string(f"ddeutil.workflow.{t}")
302
+ except ModuleNotFoundError:
303
+ for registry in params.engine.registry:
304
+ try:
305
+ return import_string(f"{registry}.{t}")
306
+ except ModuleNotFoundError:
307
+ continue
308
+ return import_string(f"{t}")
309
+
310
+
179
311
  class TagFunc(Protocol):
180
312
  """Tag Function Protocol"""
181
313
 
@@ -260,6 +392,10 @@ class BaseParam(BaseModel, ABC):
260
392
  "Receive value and validate typing before return valid value."
261
393
  )
262
394
 
395
+ @field_serializer("type")
396
+ def __serializer_type(self, value: str) -> str:
397
+ return value
398
+
263
399
 
264
400
  class DefaultParam(BaseParam):
265
401
  """Default Parameter that will check default if it required"""
@@ -583,7 +719,7 @@ def map_post_filter(
583
719
  else:
584
720
  value: Any = f_func(value, *args, **kwargs)
585
721
  except Exception as err:
586
- logging.warning(str(err))
722
+ logger.warning(str(err))
587
723
  raise UtilException(
588
724
  f"The post-filter function: {func_name} does not fit with "
589
725
  f"{value} (type: {type(value).__name__})."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.9
3
+ Version: 0.0.10
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -21,13 +21,11 @@ Classifier: Programming Language :: Python :: 3.12
21
21
  Requires-Python: >=3.9.13
22
22
  Description-Content-Type: text/markdown
23
23
  License-File: LICENSE
24
- Requires-Dist: fmtutil
25
24
  Requires-Dist: ddeutil-io
26
25
  Requires-Dist: python-dotenv ==1.0.1
27
- Requires-Dist: typer ==0.12.4
26
+ Requires-Dist: typer <1.0.0,==0.12.5
28
27
  Provides-Extra: api
29
- Requires-Dist: fastapi[standard] ==0.112.1 ; extra == 'api'
30
- Requires-Dist: croniter ==3.0.3 ; extra == 'api'
28
+ Requires-Dist: fastapi[standard] <1.0.0,==0.112.2 ; extra == 'api'
31
29
  Provides-Extra: schedule
32
30
  Requires-Dist: schedule <2.0.0,==1.2.2 ; extra == 'schedule'
33
31
 
@@ -39,17 +37,6 @@ Requires-Dist: schedule <2.0.0,==1.2.2 ; extra == 'schedule'
39
37
  [![gh license](https://img.shields.io/github/license/ddeutils/ddeutil-workflow)](https://github.com/ddeutils/ddeutil-workflow/blob/main/LICENSE)
40
38
  [![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
41
39
 
42
- **Table of Contents**:
43
-
44
- - [Installation](#installation)
45
- - [Getting Started](#getting-started)
46
- - [On](#on)
47
- - [Pipeline](#pipeline)
48
- - [Usage](#usage)
49
- - [Configuration](#configuration)
50
- - [Future](#future)
51
- - [Deployment](#deployment)
52
-
53
40
  The **Lightweight workflow orchestration** with less dependencies the was created
54
41
  for easy to make a simple metadata driven for data pipeline orchestration.
55
42
  It can to use for data operator by a `.yaml` template.
@@ -103,82 +90,6 @@ this package with application add-ons, you should add `app` in installation;
103
90
  > | ddeutil-workflow:python3.11 | `3.11` | :x: |
104
91
  > | ddeutil-workflow:python3.12 | `3.12` | :x: |
105
92
 
106
- ## Getting Started
107
-
108
- The main feature of this project is the `Pipeline` object that can call any
109
- registries function. The pipeline can handle everything that you want to do, it
110
- will passing parameters and catching the output for re-use it to next step.
111
-
112
- ### On
113
-
114
- The **On** is schedule object that receive crontab value and able to generate
115
- datetime value with next or previous with any start point of an input datetime.
116
-
117
- ```yaml
118
- # This file should keep under this path: `./root-path/conf-path/*`
119
- on_every_5_min:
120
- type: on.On
121
- cron: "*/5 * * * *"
122
- ```
123
-
124
- ```python
125
- from ddeutil.workflow.on import On
126
-
127
- # NOTE: Start load the on data from `.yaml` template file with this key.
128
- schedule = On.from_loader(name='on_every_5_min', externals={})
129
-
130
- assert '*/5 * * * *' == str(schedule.cronjob)
131
-
132
- cron_iter = schedule.generate('2022-01-01 00:00:00')
133
-
134
- assert "2022-01-01 00:05:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
135
- assert "2022-01-01 00:10:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
136
- assert "2022-01-01 00:15:00" f"{cron_iter.next:%Y-%m-%d %H:%M:%S}"
137
- ```
138
-
139
- ### Pipeline
140
-
141
- The **Pipeline** object that is the core feature of this project.
142
-
143
- ```yaml
144
- # This file should keep under this path: `./root-path/conf-path/*`
145
- pipeline-name:
146
- type: ddeutil.workflow.pipeline.Pipeline
147
- on: 'on_every_5_min'
148
- params:
149
- author-run:
150
- type: str
151
- run-date:
152
- type: datetime
153
- jobs:
154
- first-job:
155
- stages:
156
- - name: "Empty stage do logging to console only!!"
157
- ```
158
-
159
- ```python
160
- from ddeutil.workflow.pipeline import Pipeline
161
-
162
- pipe = Pipeline.from_loader(name='pipeline-name', externals={})
163
- pipe.execute(params={'author-run': 'Local Workflow', 'run-date': '2024-01-01'})
164
- ```
165
-
166
- > [!NOTE]
167
- > The above parameter can use short declarative statement. You can pass a parameter
168
- > type to the key of a parameter name but it does not handler default value if you
169
- > run this pipeline workflow with schedule.
170
- >
171
- > ```yaml
172
- > ...
173
- > params:
174
- > author-run: str
175
- > run-date: datetime
176
- > ...
177
- > ```
178
- >
179
- > And for the type, you can remove `ddeutil.workflow` prefix because we can find
180
- > it by looping search from `WORKFLOW_CORE_REGISTRY` value.
181
-
182
93
  ## Usage
183
94
 
184
95
  This is examples that use workflow file for running common Data Engineering
@@ -209,7 +120,9 @@ run_py_local:
209
120
  url: https://open-data/
210
121
  auth: ${API_ACCESS_REFRESH_TOKEN}
211
122
  aws_s3_path: my-data/open-data/
212
- # This Authentication code should implement with your custom hook function
123
+
124
+ # This Authentication code should implement with your custom hook function.
125
+ # The template allow you to use environment variable.
213
126
  aws_access_client_id: ${AWS_ACCESS_CLIENT_ID}
214
127
  aws_access_client_secret: ${AWS_ACCESS_CLIENT_SECRET}
215
128
  ```
@@ -227,28 +140,24 @@ run_py_local:
227
140
  | `WORKFLOW_CORE_STAGE_RAISE_ERROR` | Core | true | A flag that all stage raise StageException from stage execution |
228
141
  | `WORKFLOW_CORE_MAX_PIPELINE_POKING` | Core | 4 | |
229
142
  | `WORKFLOW_CORE_MAX_JOB_PARALLEL` | Core | 2 | The maximum job number that able to run parallel in pipeline executor |
143
+ | `WORKFLOW_LOG_DEBUG_MODE` | Log | true | A flag that enable logging with debug level mode |
230
144
  | `WORKFLOW_LOG_ENABLE_WRITE` | Log | true | A flag that enable logging object saving log to its destination |
231
145
 
232
146
 
233
147
  **Application**:
234
148
 
235
- | Environment | Default | Description |
236
- |-------------------------------------|---------|-------------|
237
- | `WORKFLOW_APP_PROCESS_WORKER` | 2 | |
238
- | `WORKFLOW_APP_PIPELINE_PER_PROCESS` | 100 | |
149
+ | Environment | Default | Description |
150
+ |-------------------------------------|----------------------------------|-------------------------------------------------------------------------|
151
+ | `WORKFLOW_APP_PROCESS_WORKER` | 2 | The maximum process worker number that run in scheduler app module |
152
+ | `WORKFLOW_APP_SCHEDULE_PER_PROCESS` | 100 | A schedule per process that run parallel |
153
+ | `WORKFLOW_APP_STOP_BOUNDARY_DELTA` | '{"minutes": 5, "seconds": 20}' | A time delta value that use to stop scheduler app in json string format |
239
154
 
240
155
  **API server**:
241
156
 
242
- | Environment | Default | Description |
243
- |-----------------------|--------------------------------------------------------|--------------------------------------------------------------------|
244
- | `WORKFLOW_API_DB_URL` | postgresql+asyncpg://user:pass@localhost:5432/schedule | A Database URL that will pass to SQLAlchemy create_engine function |
245
-
246
- ## Future
247
-
248
- The current milestone that will develop and necessary features that should to
249
- implement on this project.
250
-
251
- - ...
157
+ | Environment | Default | Description |
158
+ |--------------------------------------|---------|-----------------------------------------------------------------------------------|
159
+ | `WORKFLOW_API_ENABLE_ROUTE_WORKFLOW` | true | A flag that enable workflow route to manage execute manually and workflow logging |
160
+ | `WORKFLOW_API_ENABLE_ROUTE_SCHEDULE` | true | A flag that enable run scheduler |
252
161
 
253
162
  ## Deployment
254
163
 
@@ -270,4 +179,4 @@ like crontab job but via Python API.
270
179
 
271
180
  > [!NOTE]
272
181
  > If this package already deploy, it able to use
273
- > `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80`
182
+ > `uvicorn ddeutil.workflow.api:app --host 0.0.0.0 --port 80 --workers 4`
@@ -0,0 +1,21 @@
1
+ ddeutil/workflow/__about__.py,sha256=KJfEGSDA5LiPvdupul6ulxozLD2x2GhgUvq8t60EXsI,28
2
+ ddeutil/workflow/__init__.py,sha256=oGvg_BpKKb_FG76DlMvXTKD7BsYhqF9wB1r4x5Q_lQI,647
3
+ ddeutil/workflow/__types.py,sha256=SYMoxbENQX8uPsiCZkjtpHAqqHOh8rUrarAFicAJd0E,1773
4
+ ddeutil/workflow/api.py,sha256=WHgmjvnnkM4djwHt4bsAqsQjjcjAITRSrNrYYO6bgn8,2582
5
+ ddeutil/workflow/cli.py,sha256=snJCM-LAqvWwhkSB-3KRWwcgbHAkHn4cZ_DtmfOL5gs,3360
6
+ ddeutil/workflow/cron.py,sha256=uhp3E5pl_tX_H88bsDujcwdhZmOE53csyV-ouPpPdK8,25321
7
+ ddeutil/workflow/exceptions.py,sha256=UHojJQmnG9OVuRhXBAzDW6KZn-uKxvxV034QhUBUzUI,686
8
+ ddeutil/workflow/log.py,sha256=a5L5KWEGS5oiY_y6jugeAoRyAcnAhlt1HfeTU77YeI4,6036
9
+ ddeutil/workflow/on.py,sha256=Sxwnu0vPbIrMR_WWvH3_rOvD0tbiJntcB5378WoV19M,7163
10
+ ddeutil/workflow/pipeline.py,sha256=lPw9R3gOnBcU2eogClG8b4e4rTvpn5EbACLNZDuuR38,40825
11
+ ddeutil/workflow/repeat.py,sha256=0O8voTRB8lNMWsk1AbOYcio_b2_CW98yrfiEzNBb6gA,4954
12
+ ddeutil/workflow/route.py,sha256=pcn_oDzc2nl6txFhu_TWAnntLggEOFV9A3EVdnazcHI,2597
13
+ ddeutil/workflow/scheduler.py,sha256=Vu9FZbiHDnshQ2O1SnkVX686eSfaZzip-1oQohfuH_Y,20140
14
+ ddeutil/workflow/stage.py,sha256=XZEPImipk83kNX9UHrwu7wWUBigXZpEkWqagOG0oS70,20656
15
+ ddeutil/workflow/utils.py,sha256=ehIcT_fIQL8N0wU16VJDKAFN9q4h1FyMxyT5uTeMIA0,28561
16
+ ddeutil_workflow-0.0.10.dist-info/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
17
+ ddeutil_workflow-0.0.10.dist-info/METADATA,sha256=7jdDYS2WtaZFpwUCo4ur8NAFWyi-omELh8YLB3EL9ok,9433
18
+ ddeutil_workflow-0.0.10.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
19
+ ddeutil_workflow-0.0.10.dist-info/entry_points.txt,sha256=0BVOgO3LdUdXVZ-CiHHDKxzEk2c8J30jEwHeKn2YCWI,62
20
+ ddeutil_workflow-0.0.10.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
21
+ ddeutil_workflow-0.0.10.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ddeutil-workflow = ddeutil.workflow.cli:cli
@@ -1,132 +0,0 @@
1
- # ------------------------------------------------------------------------------
2
- # Copyright (c) 2022 Korawich Anuttra. All rights reserved.
3
- # Licensed under the MIT License. See LICENSE in the project root for
4
- # license information.
5
- # ------------------------------------------------------------------------------
6
- from __future__ import annotations
7
-
8
- from collections.abc import Iterator
9
- from functools import cached_property
10
- from typing import TypeVar
11
-
12
- from ddeutil.core import import_string
13
- from ddeutil.io import PathSearch, YamlFlResolve
14
- from pydantic import BaseModel
15
-
16
- from .__types import DictData
17
- from .utils import ConfParams, config
18
-
19
- AnyModel = TypeVar("AnyModel", bound=BaseModel)
20
- AnyModelType = type[AnyModel]
21
-
22
-
23
- class SimLoad:
24
- """Simple Load Object that will search config data by name.
25
-
26
- :param name: A name of config data that will read by Yaml Loader object.
27
- :param params: A Params model object.
28
- :param externals: An external parameters
29
-
30
- Noted:
31
- The config data should have ``type`` key for engine can know what is
32
- config should to do next.
33
- """
34
-
35
- def __init__(
36
- self,
37
- name: str,
38
- params: ConfParams,
39
- externals: DictData | None = None,
40
- ) -> None:
41
- self.data: DictData = {}
42
- for file in PathSearch(params.engine.paths.conf).files:
43
- if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
44
- data := YamlFlResolve(file).read().get(name, {})
45
- ):
46
- self.data = data
47
- if not self.data:
48
- raise ValueError(f"Config {name!r} does not found on conf path")
49
-
50
- # TODO: Validate the version of template data that mean if version of
51
- # Template were change it should raise to upgrade package version.
52
- # ---
53
- # <pipeline-name>:
54
- # version: 1
55
- # type: pipeline.Pipeline
56
- #
57
- self.conf_params: ConfParams = params
58
- self.externals: DictData = externals or {}
59
- self.data.update(self.externals)
60
-
61
- @classmethod
62
- def find(
63
- cls,
64
- obj: object,
65
- params: ConfParams,
66
- *,
67
- include: list[str] | None = None,
68
- exclude: list[str] | None = None,
69
- ) -> Iterator[tuple[str, DictData]]:
70
- """Find all object"""
71
- exclude: list[str] = exclude or []
72
- for file in PathSearch(params.engine.paths.conf).files:
73
- if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
74
- values := YamlFlResolve(file).read()
75
- ):
76
- for key, data in values.items():
77
- if key in exclude:
78
- continue
79
- if (
80
- (t := data.get("type"))
81
- and issubclass(cls.get_type(t, params), obj)
82
- and all(i in data for i in (include or data.keys()))
83
- ):
84
- yield key, data
85
-
86
- @classmethod
87
- def get_type(cls, t: str, params: ConfParams) -> AnyModelType:
88
- try:
89
- # NOTE: Auto adding module prefix if it does not set
90
- return import_string(f"ddeutil.workflow.{t}")
91
- except ModuleNotFoundError:
92
- for registry in params.engine.registry:
93
- try:
94
- return import_string(f"{registry}.{t}")
95
- except ModuleNotFoundError:
96
- continue
97
- return import_string(f"{t}")
98
-
99
- @cached_property
100
- def type(self) -> AnyModelType:
101
- """Return object of string type which implement on any registry. The
102
- object type
103
- """
104
- if not (_typ := self.data.get("type")):
105
- raise ValueError(
106
- f"the 'type' value: {_typ} does not exists in config data."
107
- )
108
- return self.get_type(_typ, self.conf_params)
109
-
110
-
111
- class Loader(SimLoad):
112
- """Loader Object that get the config `yaml` file from current path.
113
-
114
- :param name: A name of config data that will read by Yaml Loader object.
115
- :param externals: An external parameters
116
- """
117
-
118
- @classmethod
119
- def find(
120
- cls,
121
- obj,
122
- *,
123
- include: list[str] | None = None,
124
- exclude: list[str] | None = None,
125
- **kwargs,
126
- ) -> DictData:
127
- return super().find(
128
- obj=obj, params=config(), include=include, exclude=exclude
129
- )
130
-
131
- def __init__(self, name: str, externals: DictData) -> None:
132
- super().__init__(name, config(), externals)