ddeutil-workflow 0.0.26.post0__tar.gz → 0.0.26.post1__tar.gz

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.
Files changed (62) hide show
  1. {ddeutil_workflow-0.0.26.post0/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.26.post1}/PKG-INFO +1 -1
  2. ddeutil_workflow-0.0.26.post1/src/ddeutil/workflow/__about__.py +1 -0
  3. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/__init__.py +5 -2
  4. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/conf.py +144 -97
  5. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/scheduler.py +2 -2
  6. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/workflow.py +4 -4
  7. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1/src/ddeutil_workflow.egg-info}/PKG-INFO +1 -1
  8. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_conf.py +19 -8
  9. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_schedule_control.py +5 -5
  10. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_utils.py +14 -3
  11. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_utils_filter.py +4 -4
  12. ddeutil_workflow-0.0.26.post0/src/ddeutil/workflow/__about__.py +0 -1
  13. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/LICENSE +0 -0
  14. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/README.md +0 -0
  15. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/pyproject.toml +0 -0
  16. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/setup.cfg +0 -0
  17. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/__cron.py +0 -0
  18. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/__types.py +0 -0
  19. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/api/__init__.py +0 -0
  20. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/api/api.py +0 -0
  21. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/api/repeat.py +0 -0
  22. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/api/route.py +0 -0
  23. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/cron.py +0 -0
  24. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/exceptions.py +0 -0
  25. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/job.py +0 -0
  26. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/params.py +0 -0
  27. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/result.py +0 -0
  28. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/stage.py +0 -0
  29. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil/workflow/utils.py +0 -0
  30. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  31. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  32. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  33. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  34. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test__cron.py +0 -0
  35. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test__regex.py +0 -0
  36. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_conf_log.py +0 -0
  37. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_cron_on.py +0 -0
  38. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_job.py +0 -0
  39. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_job_exec_py.py +0 -0
  40. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_job_exec_strategy.py +0 -0
  41. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_job_strategy.py +0 -0
  42. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_params.py +0 -0
  43. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_result.py +0 -0
  44. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_schedule.py +0 -0
  45. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_schedule_tasks.py +0 -0
  46. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_stage.py +0 -0
  47. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_stage_exec_bash.py +0 -0
  48. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_stage_exec_hook.py +0 -0
  49. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_stage_exec_py.py +0 -0
  50. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_stage_exec_trigger.py +0 -0
  51. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_utils_tag.py +0 -0
  52. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_utils_template.py +0 -0
  53. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow.py +0 -0
  54. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_exec.py +0 -0
  55. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_exec_hook.py +0 -0
  56. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_exec_needs.py +0 -0
  57. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_job_exec.py +0 -0
  58. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_poke.py +0 -0
  59. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_release.py +0 -0
  60. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_release_and_queue.py +0 -0
  61. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_schedule.py +0 -0
  62. {ddeutil_workflow-0.0.26.post0 → ddeutil_workflow-0.0.26.post1}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.26.post0
3
+ Version: 0.0.26.post1
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.26.post1"
@@ -3,11 +3,14 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- from .__cron import CronRunner
6
+ from .__cron import CronJob, CronRunner
7
7
  from .conf import (
8
8
  Config,
9
- FileLog,
10
9
  Loader,
10
+ Log,
11
+ env,
12
+ get_log,
13
+ get_logger,
11
14
  )
12
15
  from .cron import (
13
16
  On,
@@ -13,27 +13,30 @@ from collections.abc import Iterator
13
13
  from datetime import datetime, timedelta
14
14
  from functools import cached_property, lru_cache
15
15
  from pathlib import Path
16
- from typing import ClassVar, Optional, TypeVar, Union
16
+ from typing import ClassVar, Optional, Union
17
17
  from zoneinfo import ZoneInfo
18
18
 
19
19
  from ddeutil.core import str2bool
20
20
  from ddeutil.io import YamlFlResolve
21
- from dotenv import load_dotenv
22
21
  from pydantic import BaseModel, Field
23
22
  from pydantic.functional_validators import model_validator
24
23
  from typing_extensions import Self
25
24
 
26
25
  from .__types import DictData, TupleStr
27
26
 
28
- AnyModel = TypeVar("AnyModel", bound=BaseModel)
29
- AnyModelType = type[AnyModel]
30
27
 
31
- load_dotenv()
28
+ def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
29
+ return os.getenv(f"WORKFLOW_{var}", default)
30
+
31
+
32
+ def glob_files(path: Path) -> Iterator[Path]: # pragma: no cov
33
+ yield from (file for file in path.rglob("*") if file.is_file())
32
34
 
33
- env = os.getenv
34
35
 
35
36
  __all__: TupleStr = (
37
+ "env",
36
38
  "get_logger",
39
+ "get_log",
37
40
  "Config",
38
41
  "SimLoad",
39
42
  "Loader",
@@ -52,6 +55,14 @@ def get_logger(name: str):
52
55
  :param name: A module name that want to log.
53
56
  """
54
57
  lg = logging.getLogger(name)
58
+
59
+ # NOTE: Developers using this package can then disable all logging just for
60
+ # this package by;
61
+ #
62
+ # `logging.getLogger('ddeutil.workflow').propagate = False`
63
+ #
64
+ lg.addHandler(logging.NullHandler())
65
+
55
66
  formatter = logging.Formatter(
56
67
  fmt=(
57
68
  "%(asctime)s.%(msecs)03d (%(name)-10s, %(process)-5d, "
@@ -68,115 +79,141 @@ def get_logger(name: str):
68
79
  return lg
69
80
 
70
81
 
71
- class Config:
82
+ class Config: # pragma: no cov
72
83
  """Config object for keeping application configuration on current session
73
84
  without changing when if the application still running.
74
85
  """
75
86
 
76
87
  # NOTE: Core
77
- root_path: Path = Path(os.getenv("WORKFLOW_ROOT_PATH", "."))
78
- tz: ZoneInfo = ZoneInfo(env("WORKFLOW_CORE_TIMEZONE", "UTC"))
79
- gen_id_simple_mode: bool = str2bool(
80
- os.getenv("WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE", "true")
81
- )
88
+ @property
89
+ def root_path(self) -> Path:
90
+ return Path(env("ROOT_PATH", "."))
91
+
92
+ @property
93
+ def conf_path(self) -> Path:
94
+ """Config path that use root_path class argument for this construction.
95
+
96
+ :rtype: Path
97
+ """
98
+ return self.root_path / env("CORE_PATH_CONF", "conf")
99
+
100
+ @property
101
+ def tz(self) -> ZoneInfo:
102
+ return ZoneInfo(env("CORE_TIMEZONE", "UTC"))
103
+
104
+ @property
105
+ def gen_id_simple_mode(self) -> bool:
106
+ return str2bool(env("CORE_GENERATE_ID_SIMPLE_MODE", "true"))
82
107
 
83
108
  # NOTE: Register
84
- regis_hook_str: str = os.getenv(
85
- "WORKFLOW_CORE_REGISTRY", "src,src.ddeutil.workflow,tests,tests.utils"
86
- )
87
- regis_filter_str: str = os.getenv(
88
- "WORKFLOW_CORE_REGISTRY_FILTER", "ddeutil.workflow.utils"
89
- )
109
+ @property
110
+ def regis_hook(self) -> list[str]:
111
+ regis_hook_str: str = env(
112
+ "CORE_REGISTRY", "src,src.ddeutil.workflow,tests,tests.utils"
113
+ )
114
+ return [r.strip() for r in regis_hook_str.split(",")]
115
+
116
+ @property
117
+ def regis_filter(self) -> list[str]:
118
+ regis_filter_str: str = env(
119
+ "CORE_REGISTRY_FILTER", "ddeutil.workflow.utils"
120
+ )
121
+ return [r.strip() for r in regis_filter_str.split(",")]
90
122
 
91
123
  # NOTE: Logging
92
- debug: bool = str2bool(os.getenv("WORKFLOW_LOG_DEBUG_MODE", "true"))
93
- enable_write_log: bool = str2bool(
94
- os.getenv("WORKFLOW_LOG_ENABLE_WRITE", "false")
95
- )
96
- log_path: Path = Path(os.getenv("WORKFLOW_LOG_PATH", "./logs"))
124
+ @property
125
+ def log_path(self) -> Path:
126
+ return Path(env("LOG_PATH", "./logs"))
127
+
128
+ @property
129
+ def debug(self) -> bool:
130
+ return str2bool(env("LOG_DEBUG_MODE", "true"))
131
+
132
+ @property
133
+ def enable_write_log(self) -> bool:
134
+ return str2bool(env("LOG_ENABLE_WRITE", "false"))
97
135
 
98
136
  # NOTE: Stage
99
- stage_raise_error: bool = str2bool(
100
- env("WORKFLOW_CORE_STAGE_RAISE_ERROR", "false")
101
- )
102
- stage_default_id: bool = str2bool(
103
- env("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false")
104
- )
137
+ @property
138
+ def stage_raise_error(self) -> bool:
139
+ return str2bool(env("CORE_STAGE_RAISE_ERROR", "false"))
105
140
 
106
- # NOTE: Job
107
- job_raise_error: bool = str2bool(
108
- env("WORKFLOW_CORE_JOB_RAISE_ERROR", "true")
109
- )
110
- job_default_id: bool = str2bool(
111
- env("WORKFLOW_CORE_JOB_DEFAULT_ID", "false")
112
- )
141
+ @property
142
+ def stage_default_id(self) -> bool:
143
+ return str2bool(env("CORE_STAGE_DEFAULT_ID", "false"))
113
144
 
114
- # NOTE: Workflow
115
- max_job_parallel: int = int(env("WORKFLOW_CORE_MAX_JOB_PARALLEL", "2"))
116
- max_job_exec_timeout: int = int(
117
- env("WORKFLOW_CORE_MAX_JOB_EXEC_TIMEOUT", "600")
118
- )
119
- max_poking_pool_worker: int = int(
120
- os.getenv("WORKFLOW_CORE_MAX_NUM_POKING", "4")
121
- )
122
- max_on_per_workflow: int = int(
123
- env("WORKFLOW_CORE_MAX_CRON_PER_WORKFLOW", "5")
124
- )
125
- max_queue_complete_hist: int = int(
126
- os.getenv("WORKFLOW_CORE_MAX_QUEUE_COMPLETE_HIST", "16")
127
- )
145
+ # NOTE: Job
146
+ @property
147
+ def job_raise_error(self) -> bool:
148
+ return str2bool(env("CORE_JOB_RAISE_ERROR", "true"))
128
149
 
129
- # NOTE: Schedule App
130
- max_schedule_process: int = int(env("WORKFLOW_APP_MAX_PROCESS", "2"))
131
- max_schedule_per_process: int = int(
132
- env("WORKFLOW_APP_MAX_SCHEDULE_PER_PROCESS", "100")
133
- )
134
- stop_boundary_delta_str: str = env(
135
- "WORKFLOW_APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
136
- )
150
+ @property
151
+ def job_default_id(self) -> bool:
152
+ return str2bool(env("CORE_JOB_DEFAULT_ID", "false"))
137
153
 
138
- # NOTE: API
139
- prefix_path: str = env("WORKFLOW_API_PREFIX_PATH", "/api/v1")
140
- enable_route_workflow: bool = str2bool(
141
- env("WORKFLOW_API_ENABLE_ROUTE_WORKFLOW", "true")
142
- )
143
- enable_route_schedule: bool = str2bool(
144
- env("WORKFLOW_API_ENABLE_ROUTE_SCHEDULE", "true")
145
- )
154
+ # NOTE: Workflow
155
+ @property
156
+ def max_job_parallel(self) -> int:
157
+ max_job_parallel = int(env("CORE_MAX_JOB_PARALLEL", "2"))
146
158
 
147
- def __init__(self) -> None:
148
159
  # VALIDATE: the MAX_JOB_PARALLEL value should not less than 0.
149
- if self.max_job_parallel < 0:
160
+ if max_job_parallel < 0:
150
161
  raise ValueError(
151
- f"``MAX_JOB_PARALLEL`` should more than 0 but got "
152
- f"{self.max_job_parallel}."
162
+ f"``WORKFLOW_MAX_JOB_PARALLEL`` should more than 0 but got "
163
+ f"{max_job_parallel}."
153
164
  )
165
+ return max_job_parallel
166
+
167
+ @property
168
+ def max_job_exec_timeout(self) -> int:
169
+ return int(env("CORE_MAX_JOB_EXEC_TIMEOUT", "600"))
170
+
171
+ @property
172
+ def max_poking_pool_worker(self) -> int:
173
+ return int(env("CORE_MAX_NUM_POKING", "4"))
174
+
175
+ @property
176
+ def max_on_per_workflow(self) -> int:
177
+ return int(env("CORE_MAX_CRON_PER_WORKFLOW", "5"))
178
+
179
+ @property
180
+ def max_queue_complete_hist(self) -> int:
181
+ return int(env("CORE_MAX_QUEUE_COMPLETE_HIST", "16"))
182
+
183
+ # NOTE: Schedule App
184
+ @property
185
+ def max_schedule_process(self) -> int:
186
+ return int(env("APP_MAX_PROCESS", "2"))
187
+
188
+ @property
189
+ def max_schedule_per_process(self) -> int:
190
+ return int(env("APP_MAX_SCHEDULE_PER_PROCESS", "100"))
154
191
 
192
+ @property
193
+ def stop_boundary_delta(self) -> timedelta:
194
+ stop_boundary_delta_str: str = env(
195
+ "APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
196
+ )
155
197
  try:
156
- self.stop_boundary_delta: timedelta = timedelta(
157
- **json.loads(self.stop_boundary_delta_str)
158
- )
198
+ return timedelta(**json.loads(stop_boundary_delta_str))
159
199
  except Exception as err:
160
200
  raise ValueError(
161
201
  "Config ``WORKFLOW_APP_STOP_BOUNDARY_DELTA`` can not parsing to"
162
- f"timedelta with {self.stop_boundary_delta_str}."
202
+ f"timedelta with {stop_boundary_delta_str}."
163
203
  ) from err
164
204
 
205
+ # NOTE: API
165
206
  @property
166
- def conf_path(self) -> Path:
167
- """Config path that use root_path class argument for this construction.
168
-
169
- :rtype: Path
170
- """
171
- return self.root_path / os.getenv("WORKFLOW_CORE_PATH_CONF", "conf")
207
+ def prefix_path(self) -> str:
208
+ return env("API_PREFIX_PATH", "/api/v1")
172
209
 
173
210
  @property
174
- def regis_hook(self) -> list[str]:
175
- return [r.strip() for r in self.regis_hook_str.split(",")]
211
+ def enable_route_workflow(self) -> bool:
212
+ return str2bool(env("API_ENABLE_ROUTE_WORKFLOW", "true"))
176
213
 
177
214
  @property
178
- def regis_filter(self) -> list[str]:
179
- return [r.strip() for r in self.regis_filter_str.split(",")]
215
+ def enable_route_schedule(self) -> bool:
216
+ return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
180
217
 
181
218
 
182
219
  class SimLoad:
@@ -206,14 +243,9 @@ class SimLoad:
206
243
  externals: DictData | None = None,
207
244
  ) -> None:
208
245
  self.data: DictData = {}
209
- for file in conf.conf_path.rglob("*"):
210
- if not file.is_file():
211
- continue
212
-
213
- if data := self.filter_suffix(
214
- file,
215
- name,
216
- ):
246
+ for file in glob_files(conf.conf_path):
247
+
248
+ if data := self.filter_suffix(file, name):
217
249
  self.data = data
218
250
 
219
251
  # VALIDATE: check the data that reading should not empty.
@@ -245,10 +277,7 @@ class SimLoad:
245
277
  :rtype: Iterator[tuple[str, DictData]]
246
278
  """
247
279
  exclude: list[str] = excluded or []
248
- for file in conf.conf_path.rglob("*"):
249
-
250
- if not file.is_file():
251
- continue
280
+ for file in glob_files(conf.conf_path):
252
281
 
253
282
  for key, data in cls.filter_suffix(file).items():
254
283
 
@@ -274,7 +303,7 @@ class SimLoad:
274
303
  """Return object of string type which implement on any registry. The
275
304
  object type.
276
305
 
277
- :rtype: AnyModelType
306
+ :rtype: str
278
307
  """
279
308
  if _typ := self.data.get("type"):
280
309
  return _typ
@@ -481,6 +510,18 @@ class FileLog(BaseLog):
481
510
 
482
511
  class SQLiteLog(BaseLog): # pragma: no cov
483
512
 
513
+ table: str = "workflow_log"
514
+ ddl: str = """
515
+ workflow str,
516
+ release int,
517
+ type str,
518
+ context json,
519
+ parent_run_id int,
520
+ run_id int,
521
+ update datetime
522
+ primary key ( run_id )
523
+ """
524
+
484
525
  def save(self, excluded: list[str] | None) -> None:
485
526
  raise NotImplementedError("SQLiteLog does not implement yet.")
486
527
 
@@ -489,3 +530,9 @@ Log = Union[
489
530
  FileLog,
490
531
  SQLiteLog,
491
532
  ]
533
+
534
+
535
+ def get_log() -> Log: # pragma: no cov
536
+ if config.log_path.is_file():
537
+ return SQLiteLog
538
+ return FileLog
@@ -51,7 +51,7 @@ except ImportError: # pragma: no cov
51
51
 
52
52
  from .__cron import CronRunner
53
53
  from .__types import DictData, TupleStr
54
- from .conf import FileLog, Loader, Log, config, get_logger
54
+ from .conf import Loader, Log, config, get_log, get_logger
55
55
  from .cron import On
56
56
  from .exceptions import WorkflowException
57
57
  from .utils import (
@@ -493,7 +493,7 @@ def schedule_control(
493
493
  "Should install schedule package before use this module."
494
494
  ) from None
495
495
 
496
- log: type[Log] = log or FileLog
496
+ log: type[Log] = log or get_log()
497
497
  scheduler: Scheduler = Scheduler()
498
498
  start_date: datetime = datetime.now(tz=config.tz)
499
499
  stop_date: datetime = stop or (start_date + config.stop_boundary_delta)
@@ -42,7 +42,7 @@ from typing_extensions import Self
42
42
 
43
43
  from .__cron import CronJob, CronRunner
44
44
  from .__types import DictData, TupleStr
45
- from .conf import FileLog, Loader, Log, config, get_logger
45
+ from .conf import Loader, Log, config, get_log, get_logger
46
46
  from .cron import On
47
47
  from .exceptions import JobException, WorkflowException
48
48
  from .job import Job
@@ -501,7 +501,7 @@ class Workflow(BaseModel):
501
501
 
502
502
  :rtype: Result
503
503
  """
504
- log: type[Log] = log or FileLog
504
+ log: type[Log] = log or get_log()
505
505
  name: str = override_log_name or self.name
506
506
  run_id: str = run_id or gen_id(name, unique=True)
507
507
  rs_release: Result = Result(run_id=run_id)
@@ -670,7 +670,7 @@ class Workflow(BaseModel):
670
670
  :rtype: list[Result]
671
671
  :return: A list of all results that return from ``self.release`` method.
672
672
  """
673
- log: type[Log] = log or FileLog
673
+ log: type[Log] = log or get_log()
674
674
  run_id: str = run_id or gen_id(self.name, unique=True)
675
675
 
676
676
  # NOTE: If this workflow does not set the on schedule, it will return
@@ -1151,7 +1151,7 @@ class WorkflowTask:
1151
1151
 
1152
1152
  :rtype: Result
1153
1153
  """
1154
- log: type[Log] = log or FileLog
1154
+ log: type[Log] = log or get_log()
1155
1155
 
1156
1156
  if release is None:
1157
1157
  if queue.check_queue(self.runner.date):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.26.post0
3
+ Version: 0.0.26.post1
4
4
  Summary: Lightweight workflow orchestration with less dependencies
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -13,18 +13,29 @@ from ddeutil.workflow.scheduler import Schedule
13
13
  from ddeutil.workflow.workflow import Workflow
14
14
 
15
15
 
16
- def test_config():
17
- with mock.patch.object(Config, "max_job_parallel", -1):
18
- with pytest.raises(ValueError):
19
- Config()
16
+ @pytest.fixture(scope="function")
17
+ def adjust_config():
18
+ origin_max_job = os.getenv("WORKFLOW_CORE_MAX_JOB_PARALLEL")
19
+ origin_stop = os.getenv("WORKFLOW_APP_STOP_BOUNDARY_DELTA")
20
+ os.environ["WORKFLOW_CORE_MAX_JOB_PARALLEL"] = "-1"
21
+ os.environ["WORKFLOW_APP_STOP_BOUNDARY_DELTA"] = "{"
20
22
 
21
- with mock.patch.object(Config, "stop_boundary_delta_str", "{"):
22
- with pytest.raises(ValueError):
23
- Config()
23
+ yield
24
+
25
+ os.environ["WORKFLOW_CORE_MAX_JOB_PARALLEL"] = origin_max_job
26
+ os.environ["WORKFLOW_APP_STOP_BOUNDARY_DELTA"] = origin_stop
27
+
28
+
29
+ def test_config(adjust_config):
30
+
31
+ with pytest.raises(ValueError):
32
+ _ = Config().max_job_parallel
33
+
34
+ with pytest.raises(ValueError):
35
+ _ = Config().stop_boundary_delta
24
36
 
25
37
  conf = Config()
26
38
  os.environ["WORKFLOW_CORE_TIMEZONE"] = "Asia/Bangkok"
27
- conf = conf.refresh_dotenv()
28
39
  assert conf.tz == ZoneInfo("Asia/Bangkok")
29
40
 
30
41
 
@@ -1,31 +1,31 @@
1
1
  from datetime import timedelta
2
2
  from unittest import mock
3
3
 
4
- from ddeutil.workflow.conf import Config, config
4
+ from ddeutil.workflow.conf import Config
5
5
  from ddeutil.workflow.scheduler import schedule_control
6
6
 
7
7
 
8
- @mock.patch.object(config, "stop_boundary_delta", timedelta(minutes=1))
8
+ @mock.patch.object(Config, "stop_boundary_delta", timedelta(minutes=1))
9
9
  @mock.patch.object(Config, "enable_write_log", False)
10
10
  def test_schedule_control():
11
11
  rs = schedule_control(["schedule-every-minute-wf"])
12
12
  assert rs == ["schedule-every-minute-wf"]
13
13
 
14
14
 
15
- @mock.patch.object(config, "stop_boundary_delta", timedelta(minutes=3))
15
+ @mock.patch.object(Config, "stop_boundary_delta", timedelta(minutes=3))
16
16
  @mock.patch.object(Config, "enable_write_log", False)
17
17
  def test_schedule_control_multi_on():
18
18
  rs = schedule_control(["schedule-multi-on-wf"])
19
19
  assert rs == ["schedule-multi-on-wf"]
20
20
 
21
21
 
22
- @mock.patch.object(config, "stop_boundary_delta", timedelta(minutes=0))
22
+ @mock.patch.object(Config, "stop_boundary_delta", timedelta(minutes=0))
23
23
  def test_schedule_control_stop():
24
24
  rs = schedule_control(["schedule-every-minute-wf"])
25
25
  assert rs == ["schedule-every-minute-wf"]
26
26
 
27
27
 
28
- @mock.patch.object(config, "stop_boundary_delta", timedelta(minutes=2))
28
+ @mock.patch.object(Config, "stop_boundary_delta", timedelta(minutes=2))
29
29
  @mock.patch.object(Config, "enable_write_log", False)
30
30
  def test_schedule_control_parallel():
31
31
  rs = schedule_control(["schedule-every-minute-wf-parallel"])
@@ -1,16 +1,27 @@
1
+ import os
1
2
  from pathlib import Path
2
- from unittest.mock import patch
3
3
 
4
4
  import ddeutil.workflow.utils as utils
5
5
  import pytest
6
6
 
7
7
 
8
+ @pytest.fixture(scope="function")
9
+ def adjust_config_gen_id():
10
+ origin_simple = os.getenv("WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE")
11
+ os.environ["WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE"] = "false"
12
+
13
+ yield
14
+
15
+ os.environ["WORKFLOW_CORE_GENERATE_ID_SIMPLE_MODE"] = origin_simple
16
+
17
+
8
18
  def test_gen_id():
9
19
  assert "1354680202" == utils.gen_id("{}")
10
20
  assert "1354680202" == utils.gen_id("{}", sensitive=False)
11
21
 
12
- with patch("ddeutil.workflow.utils.config.gen_id_simple_mode", False):
13
- assert "99914b932bd37a50b983c5e7c90ae93b" == utils.gen_id("{}")
22
+
23
+ def test_gen_id_not_simple(adjust_config_gen_id):
24
+ assert "99914b932bd37a50b983c5e7c90ae93b" == utils.gen_id("{}")
14
25
 
15
26
 
16
27
  def test_filter_func():
@@ -25,8 +25,8 @@ def raise_err(_: str) -> None: # pragma: no cov
25
25
  def test_make_registry_raise():
26
26
  with mock.patch.object(
27
27
  Config,
28
- "regis_filter_str",
29
- "ddeutil.workflow.utils,tests.test_utils_filter,foo.bar",
28
+ "regis_filter",
29
+ ["ddeutil.workflow.utils", "tests.test_utils_filter", "foo.bar"],
30
30
  ):
31
31
  assert isfunction(make_filter_registry()["foo"])
32
32
  assert "bar" == make_filter_registry()["foo"]("")
@@ -59,8 +59,8 @@ def test_get_args_const():
59
59
  def test_map_post_filter():
60
60
  with mock.patch.object(
61
61
  Config,
62
- "regis_filter_str",
63
- "ddeutil.workflow.utils,tests.test_utils_filter,foo.bar",
62
+ "regis_filter",
63
+ ["ddeutil.workflow.utils", "tests.test_utils_filter", "foo.bar"],
64
64
  ):
65
65
  assert "bar" == map_post_filter("demo", ["foo"], make_filter_registry())
66
66
  assert "'bar'" == map_post_filter(
@@ -1 +0,0 @@
1
- __version__: str = "0.0.26.post0"