ddeutil-workflow 0.0.55__py3-none-any.whl → 0.0.57__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/__about__.py +1 -1
- ddeutil/workflow/__cron.py +26 -12
- ddeutil/workflow/__init__.py +4 -2
- ddeutil/workflow/__main__.py +30 -0
- ddeutil/workflow/__types.py +1 -0
- ddeutil/workflow/conf.py +163 -101
- ddeutil/workflow/{cron.py → event.py} +37 -20
- ddeutil/workflow/exceptions.py +44 -14
- ddeutil/workflow/job.py +87 -58
- ddeutil/workflow/logs.py +13 -5
- ddeutil/workflow/result.py +9 -4
- ddeutil/workflow/scheduler.py +38 -73
- ddeutil/workflow/stages.py +370 -147
- ddeutil/workflow/utils.py +37 -6
- ddeutil/workflow/workflow.py +243 -302
- {ddeutil_workflow-0.0.55.dist-info → ddeutil_workflow-0.0.57.dist-info}/METADATA +41 -35
- ddeutil_workflow-0.0.57.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.55.dist-info → ddeutil_workflow-0.0.57.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.57.dist-info/entry_points.txt +2 -0
- ddeutil_workflow-0.0.55.dist-info/RECORD +0 -30
- {ddeutil_workflow-0.0.55.dist-info → ddeutil_workflow-0.0.57.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.55.dist-info → ddeutil_workflow-0.0.57.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.57"
|
ddeutil/workflow/__cron.py
CHANGED
@@ -502,10 +502,10 @@ class CronPart:
|
|
502
502
|
except IndexError:
|
503
503
|
next_value: int = -1
|
504
504
|
if value != (next_value - 1):
|
505
|
-
# NOTE:
|
505
|
+
# NOTE: `next_value` is not the subsequent number
|
506
506
|
if start_number is None:
|
507
507
|
# NOTE:
|
508
|
-
# The last number of the list
|
508
|
+
# The last number of the list `self.values` is not in a
|
509
509
|
# range.
|
510
510
|
multi_dim_values.append(value)
|
511
511
|
else:
|
@@ -703,11 +703,14 @@ class CronJob:
|
|
703
703
|
*,
|
704
704
|
tz: str | None = None,
|
705
705
|
) -> CronRunner:
|
706
|
-
"""Returns
|
707
|
-
|
706
|
+
"""Returns CronRunner instance that be datetime runner with this
|
707
|
+
cronjob. It can use `next`, `prev`, or `reset` methods to generate
|
708
|
+
running date.
|
708
709
|
|
709
|
-
:param date: An initial date that want to mark as the start
|
710
|
-
|
710
|
+
:param date: (datetime) An initial date that want to mark as the start
|
711
|
+
point. (Default is use the current datetime)
|
712
|
+
:param tz: (str) A string timezone that want to change on runner.
|
713
|
+
(Default is None)
|
711
714
|
|
712
715
|
:rtype: CronRunner
|
713
716
|
"""
|
@@ -743,6 +746,10 @@ class CronJobYear(CronJob):
|
|
743
746
|
class CronRunner:
|
744
747
|
"""Create an instance of Date Runner object for datetime generate with
|
745
748
|
cron schedule object value.
|
749
|
+
|
750
|
+
:param cron: (CronJob | CronJobYear)
|
751
|
+
:param date: (datetime)
|
752
|
+
:param tz: (str)
|
746
753
|
"""
|
747
754
|
|
748
755
|
shift_limit: ClassVar[int] = 25
|
@@ -761,11 +768,17 @@ class CronRunner:
|
|
761
768
|
cron: CronJob | CronJobYear,
|
762
769
|
date: datetime | None = None,
|
763
770
|
*,
|
764
|
-
tz: str | None = None,
|
771
|
+
tz: str | ZoneInfo | None = None,
|
765
772
|
) -> None:
|
766
|
-
|
767
|
-
self.tz: ZoneInfo = ZoneInfo("UTC")
|
773
|
+
self.tz: ZoneInfo | None = None
|
768
774
|
if tz:
|
775
|
+
if isinstance(tz, ZoneInfo):
|
776
|
+
self.tz = tz
|
777
|
+
elif not isinstance(tz, str):
|
778
|
+
raise TypeError(
|
779
|
+
"Invalid type of `tz` parameter, it should be str or "
|
780
|
+
"ZoneInfo instance."
|
781
|
+
)
|
769
782
|
try:
|
770
783
|
self.tz = ZoneInfo(tz)
|
771
784
|
except ZoneInfoNotFoundError as err:
|
@@ -777,9 +790,10 @@ class CronRunner:
|
|
777
790
|
raise ValueError(
|
778
791
|
"Input schedule start time is not a valid datetime object."
|
779
792
|
)
|
780
|
-
if tz is None:
|
781
|
-
self.
|
782
|
-
|
793
|
+
if tz is not None:
|
794
|
+
self.date: datetime = date.astimezone(self.tz)
|
795
|
+
else:
|
796
|
+
self.date: datetime = date
|
783
797
|
else:
|
784
798
|
self.date: datetime = datetime.now(tz=self.tz)
|
785
799
|
|
ddeutil/workflow/__init__.py
CHANGED
@@ -7,16 +7,18 @@ from .__cron import CronJob, CronRunner
|
|
7
7
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
8
8
|
from .conf import (
|
9
9
|
Config,
|
10
|
-
|
10
|
+
FileLoad,
|
11
11
|
config,
|
12
12
|
env,
|
13
13
|
)
|
14
|
-
from .
|
14
|
+
from .event import *
|
15
15
|
from .exceptions import *
|
16
16
|
from .job import *
|
17
17
|
from .logs import (
|
18
18
|
Audit,
|
19
19
|
AuditModel,
|
20
|
+
FileAudit,
|
21
|
+
FileTrace,
|
20
22
|
Trace,
|
21
23
|
TraceData,
|
22
24
|
TraceMeta,
|
ddeutil/workflow/__main__.py
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
import typer
|
2
|
+
|
3
|
+
app = typer.Typer()
|
4
|
+
|
5
|
+
|
6
|
+
@app.callback()
|
7
|
+
def callback():
|
8
|
+
"""
|
9
|
+
Awesome Portal Gun
|
10
|
+
"""
|
11
|
+
|
12
|
+
|
13
|
+
@app.command()
|
14
|
+
def provision():
|
15
|
+
"""
|
16
|
+
Shoot the portal gun
|
17
|
+
"""
|
18
|
+
typer.echo("Shooting portal gun")
|
19
|
+
|
20
|
+
|
21
|
+
@app.command()
|
22
|
+
def job():
|
23
|
+
"""
|
24
|
+
Load the portal gun
|
25
|
+
"""
|
26
|
+
typer.echo("Loading portal gun")
|
27
|
+
|
28
|
+
|
29
|
+
if __name__ == "__main__":
|
30
|
+
app()
|
ddeutil/workflow/__types.py
CHANGED
ddeutil/workflow/conf.py
CHANGED
@@ -7,24 +7,26 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import json
|
9
9
|
import os
|
10
|
+
from abc import ABC, abstractmethod
|
10
11
|
from collections.abc import Iterator
|
11
12
|
from datetime import timedelta
|
12
13
|
from functools import cached_property
|
14
|
+
from inspect import isclass
|
13
15
|
from pathlib import Path
|
14
|
-
from typing import Final, Optional, TypeVar
|
16
|
+
from typing import Final, Optional, Protocol, TypeVar, Union
|
15
17
|
from zoneinfo import ZoneInfo
|
16
18
|
|
17
19
|
from ddeutil.core import str2bool
|
18
20
|
from ddeutil.io import YamlFlResolve
|
19
21
|
from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
20
22
|
|
21
|
-
from .__types import DictData
|
23
|
+
from .__types import DictData
|
22
24
|
|
23
25
|
T = TypeVar("T")
|
24
26
|
PREFIX: Final[str] = "WORKFLOW"
|
25
27
|
|
26
28
|
|
27
|
-
def env(var: str, default: str | None = None) -> str | None:
|
29
|
+
def env(var: str, default: str | None = None) -> str | None:
|
28
30
|
"""Get environment variable with uppercase and adding prefix string.
|
29
31
|
|
30
32
|
:param var: (str) A env variable name.
|
@@ -35,17 +37,6 @@ def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
|
35
37
|
return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
|
36
38
|
|
37
39
|
|
38
|
-
__all__: TupleStr = (
|
39
|
-
"api_config",
|
40
|
-
"env",
|
41
|
-
"Config",
|
42
|
-
"SimLoad",
|
43
|
-
"Loader",
|
44
|
-
"config",
|
45
|
-
"dynamic",
|
46
|
-
)
|
47
|
-
|
48
|
-
|
49
40
|
class Config: # pragma: no cov
|
50
41
|
"""Config object for keeping core configurations on the current session
|
51
42
|
without changing when if the application still running.
|
@@ -188,7 +179,7 @@ class Config: # pragma: no cov
|
|
188
179
|
return timedelta(**json.loads(stop_boundary_delta_str))
|
189
180
|
except Exception as err:
|
190
181
|
raise ValueError(
|
191
|
-
"Config
|
182
|
+
"Config `WORKFLOW_APP_STOP_BOUNDARY_DELTA` can not parsing to"
|
192
183
|
f"timedelta with {stop_boundary_delta_str}."
|
193
184
|
) from err
|
194
185
|
|
@@ -209,110 +200,202 @@ class APIConfig:
|
|
209
200
|
return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
|
210
201
|
|
211
202
|
|
212
|
-
class
|
213
|
-
"""
|
214
|
-
|
203
|
+
class BaseLoad(ABC): # pragma: no cov
|
204
|
+
"""Base Load object is the abstraction object for any Load object that
|
205
|
+
should to inherit from this base class.
|
206
|
+
"""
|
207
|
+
|
208
|
+
@classmethod
|
209
|
+
@abstractmethod
|
210
|
+
def find(cls, name: str, *args, **kwargs) -> DictData: ...
|
211
|
+
|
212
|
+
@classmethod
|
213
|
+
@abstractmethod
|
214
|
+
def finds(
|
215
|
+
cls, obj: object, *args, **kwargs
|
216
|
+
) -> Iterator[tuple[str, DictData]]: ...
|
215
217
|
|
216
|
-
|
217
|
-
|
218
|
-
|
218
|
+
|
219
|
+
class FileLoad(BaseLoad):
|
220
|
+
"""Base Load object that use to search config data by given some identity
|
221
|
+
value like name of `Workflow` or `On` templates.
|
222
|
+
|
223
|
+
:param name: (str) A name of key of config data that read with YAML
|
224
|
+
Environment object.
|
225
|
+
:param path: (Path) A config path object.
|
226
|
+
:param externals: (DictData) An external config data that want to add to
|
227
|
+
loaded config data.
|
228
|
+
:param extras: (DictDdata) An extra parameters that use to override core
|
229
|
+
config values.
|
230
|
+
|
231
|
+
:raise ValueError: If the data does not find on the config path with the
|
232
|
+
name parameter.
|
219
233
|
|
220
234
|
Noted:
|
221
|
-
The config data should have
|
235
|
+
The config data should have `type` key for modeling validation that
|
222
236
|
make this loader know what is config should to do pass to.
|
223
237
|
|
224
238
|
... <identity-key>:
|
225
239
|
... type: <importable-object>
|
226
240
|
... <key-data-1>: <value-data-1>
|
227
241
|
... <key-data-2>: <value-data-2>
|
242
|
+
|
243
|
+
This object support multiple config paths if you pass the `conf_paths`
|
244
|
+
key to the `extras` parameter.
|
228
245
|
"""
|
229
246
|
|
230
247
|
def __init__(
|
231
248
|
self,
|
232
249
|
name: str,
|
233
|
-
|
250
|
+
*,
|
251
|
+
path: Optional[Union[str, Path]] = None,
|
234
252
|
externals: DictData | None = None,
|
253
|
+
extras: DictData | None = None,
|
235
254
|
) -> None:
|
236
|
-
self.
|
255
|
+
self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
|
237
256
|
self.externals: DictData = externals or {}
|
238
|
-
|
239
|
-
self.data: DictData =
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
if data := self.filter_yaml(file, name=name):
|
246
|
-
self.data = data
|
257
|
+
self.extras: DictData = extras or {}
|
258
|
+
self.data: DictData = self.find(
|
259
|
+
name,
|
260
|
+
path=path,
|
261
|
+
paths=self.extras.get("conf_paths"),
|
262
|
+
extras=extras,
|
263
|
+
)
|
247
264
|
|
248
265
|
# VALIDATE: check the data that reading should not empty.
|
249
266
|
if not self.data:
|
250
267
|
raise ValueError(
|
251
|
-
f"Config {name!r} does not found on conf path: "
|
252
|
-
f"{self.conf_path}."
|
268
|
+
f"Config {name!r} does not found on the conf path: {self.path}."
|
253
269
|
)
|
254
270
|
|
255
271
|
self.data.update(self.externals)
|
256
272
|
|
273
|
+
@classmethod
|
274
|
+
def find(
|
275
|
+
cls,
|
276
|
+
name: str,
|
277
|
+
*,
|
278
|
+
path: Optional[Path] = None,
|
279
|
+
paths: Optional[list[Path]] = None,
|
280
|
+
extras: Optional[DictData] = None,
|
281
|
+
) -> DictData:
|
282
|
+
"""Find data with specific key and return the latest modify date data if
|
283
|
+
this key exists multiple files.
|
284
|
+
|
285
|
+
:param name: (str) A name of data that want to find.
|
286
|
+
:param path: (Path) A config path object.
|
287
|
+
:param paths: (list[Path]) A list of config path object.
|
288
|
+
:param extras: (DictData) An extra parameter that use to override core
|
289
|
+
config values.
|
290
|
+
|
291
|
+
:rtype: DictData
|
292
|
+
"""
|
293
|
+
path: Path = dynamic("conf_path", f=path, extras=extras)
|
294
|
+
if not paths:
|
295
|
+
paths: list[Path] = [path]
|
296
|
+
elif not isinstance(paths, list):
|
297
|
+
raise TypeError(
|
298
|
+
f"Multi-config paths does not support for type: {type(paths)}"
|
299
|
+
)
|
300
|
+
else:
|
301
|
+
paths.append(path)
|
302
|
+
|
303
|
+
all_data: list[tuple[float, DictData]] = []
|
304
|
+
for path in paths:
|
305
|
+
for file in glob_files(path):
|
306
|
+
|
307
|
+
if cls.is_ignore(file, path):
|
308
|
+
continue
|
309
|
+
|
310
|
+
if data := cls.filter_yaml(file, name=name):
|
311
|
+
all_data.append((file.lstat().st_mtime, data))
|
312
|
+
|
313
|
+
return {} if not all_data else max(all_data, key=lambda x: x[0])[1]
|
314
|
+
|
257
315
|
@classmethod
|
258
316
|
def finds(
|
259
317
|
cls,
|
260
318
|
obj: object,
|
261
|
-
conf_path: Path,
|
262
319
|
*,
|
263
|
-
|
320
|
+
path: Optional[Path] = None,
|
321
|
+
paths: Optional[list[Path]] = None,
|
264
322
|
excluded: list[str] | None = None,
|
323
|
+
extras: Optional[DictData] = None,
|
265
324
|
) -> Iterator[tuple[str, DictData]]:
|
266
325
|
"""Find all data that match with object type in config path. This class
|
267
326
|
method can use include and exclude list of identity name for filter and
|
268
327
|
adds-on.
|
269
328
|
|
270
329
|
:param obj: An object that want to validate matching before return.
|
271
|
-
:param
|
272
|
-
:param
|
273
|
-
data if any key exist.
|
330
|
+
:param path: A config path object.
|
331
|
+
:param paths: (list[Path]) A list of config path object.
|
274
332
|
:param excluded: An included list of data key that want to filter from
|
275
333
|
data.
|
334
|
+
:param extras: (DictData) An extra parameter that use to override core
|
335
|
+
config values.
|
276
336
|
|
277
337
|
:rtype: Iterator[tuple[str, DictData]]
|
278
338
|
"""
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
339
|
+
excluded: list[str] = excluded or []
|
340
|
+
path: Path = dynamic("conf_path", f=path, extras=extras)
|
341
|
+
paths: Optional[list[Path]] = paths or (extras or {}).get("conf_paths")
|
342
|
+
if not paths:
|
343
|
+
paths: list[Path] = [path]
|
344
|
+
elif not isinstance(paths, list):
|
345
|
+
raise TypeError(
|
346
|
+
f"Multi-config paths does not support for type: {type(paths)}"
|
347
|
+
)
|
348
|
+
else:
|
349
|
+
paths.append(path)
|
284
350
|
|
285
|
-
|
351
|
+
all_data: dict[str, list[tuple[float, DictData]]] = {}
|
352
|
+
for path in paths:
|
353
|
+
for file in glob_files(path):
|
286
354
|
|
287
|
-
if
|
355
|
+
if cls.is_ignore(file, path):
|
288
356
|
continue
|
289
357
|
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
358
|
+
for key, data in cls.filter_yaml(file).items():
|
359
|
+
|
360
|
+
if key in excluded:
|
361
|
+
continue
|
362
|
+
|
363
|
+
if (
|
364
|
+
data.get("type", "")
|
365
|
+
== (obj if isclass(obj) else obj.__class__).__name__
|
366
|
+
):
|
367
|
+
marking: tuple[float, DictData] = (
|
368
|
+
file.lstat().st_mtime,
|
369
|
+
data,
|
370
|
+
)
|
371
|
+
if key in all_data:
|
372
|
+
all_data[key].append(marking)
|
373
|
+
else:
|
374
|
+
all_data[key] = [marking]
|
375
|
+
|
376
|
+
for key in all_data:
|
377
|
+
yield key, max(all_data[key], key=lambda x: x[0])[1]
|
296
378
|
|
297
379
|
@classmethod
|
298
380
|
def is_ignore(
|
299
381
|
cls,
|
300
382
|
file: Path,
|
301
|
-
|
383
|
+
path: Path,
|
302
384
|
*,
|
303
385
|
ignore_filename: Optional[str] = None,
|
304
386
|
) -> bool:
|
305
387
|
"""Check this file was ignored.
|
306
388
|
|
307
389
|
:param file: (Path) A file path that want to check.
|
308
|
-
:param
|
390
|
+
:param path: (Path) A config path that want to read the config
|
309
391
|
ignore file.
|
310
|
-
:param ignore_filename: (str) An ignore filename.
|
392
|
+
:param ignore_filename: (str) An ignore filename. Default is
|
393
|
+
`.confignore` filename.
|
311
394
|
|
312
395
|
:rtype: bool
|
313
396
|
"""
|
314
397
|
ignore_filename: str = ignore_filename or ".confignore"
|
315
|
-
return is_ignored(file, read_ignore(
|
398
|
+
return is_ignored(file, read_ignore(path / ignore_filename))
|
316
399
|
|
317
400
|
@classmethod
|
318
401
|
def filter_yaml(cls, file: Path, name: str | None = None) -> DictData:
|
@@ -356,57 +439,36 @@ def dynamic(
|
|
356
439
|
"""Dynamic get config if extra value was passed at run-time.
|
357
440
|
|
358
441
|
:param key: (str) A config key that get from Config object.
|
359
|
-
:param f: An inner config function scope.
|
442
|
+
:param f: (T) An inner config function scope.
|
360
443
|
:param extras: An extra values that pass at run-time.
|
444
|
+
|
445
|
+
:rtype: T
|
361
446
|
"""
|
362
|
-
|
363
|
-
|
364
|
-
if
|
447
|
+
extra: Optional[T] = (extras or {}).get(key, None)
|
448
|
+
conf: Optional[T] = getattr(config, key, None) if f is None else f
|
449
|
+
if extra is None:
|
450
|
+
return conf
|
451
|
+
if not isinstance(extra, type(conf)):
|
365
452
|
raise TypeError(
|
366
|
-
f"Type of config {key!r} from extras: {
|
367
|
-
f"as config {type(
|
453
|
+
f"Type of config {key!r} from extras: {extra!r} does not valid "
|
454
|
+
f"as config {type(conf)}."
|
368
455
|
)
|
369
|
-
return
|
456
|
+
return extra
|
370
457
|
|
371
458
|
|
372
|
-
class Loader(
|
373
|
-
|
459
|
+
class Loader(Protocol): # pragma: no cov
|
460
|
+
type: str
|
461
|
+
path: Path
|
462
|
+
data: DictData
|
463
|
+
extras: DictData
|
464
|
+
externals: DictData
|
374
465
|
|
375
|
-
|
376
|
-
:param externals: (DictData) An external parameters
|
377
|
-
"""
|
466
|
+
def __init__(self, *args, **kwargs) -> None: ...
|
378
467
|
|
379
468
|
@classmethod
|
380
|
-
def
|
381
|
-
cls,
|
382
|
-
obj: object,
|
383
|
-
*,
|
384
|
-
path: Path | None = None,
|
385
|
-
included: list[str] | None = None,
|
386
|
-
excluded: list[str] | None = None,
|
387
|
-
**kwargs,
|
388
|
-
) -> Iterator[tuple[str, DictData]]:
|
389
|
-
"""Override the find class method from the Simple Loader object.
|
390
|
-
|
391
|
-
:param obj: An object that want to validate matching before return.
|
392
|
-
:param path: (Path) A override config path.
|
393
|
-
:param included: An excluded list of data key that want to reject this
|
394
|
-
data if any key exist.
|
395
|
-
:param excluded: An included list of data key that want to filter from
|
396
|
-
data.
|
397
|
-
|
398
|
-
:rtype: Iterator[tuple[str, DictData]]
|
399
|
-
"""
|
400
|
-
return super().finds(
|
401
|
-
obj=obj,
|
402
|
-
conf_path=(path or config.conf_path),
|
403
|
-
included=included,
|
404
|
-
excluded=excluded,
|
405
|
-
)
|
469
|
+
def find(cls, name: str, *args, **kwargs) -> DictData: ...
|
406
470
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
externals=externals,
|
412
|
-
)
|
471
|
+
@classmethod
|
472
|
+
def finds(
|
473
|
+
cls, obj: object, *args, **kwargs
|
474
|
+
) -> Iterator[tuple[str, DictData]]: ...
|
@@ -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
|
+
"""Event module that store all event object. Now, it has only `On` and `OnYear`
|
7
|
+
model these are schedule with crontab event.
|
8
|
+
"""
|
6
9
|
from __future__ import annotations
|
7
10
|
|
8
11
|
from dataclasses import fields
|
9
12
|
from datetime import datetime
|
10
|
-
from typing import Annotated, Literal, Union
|
13
|
+
from typing import Annotated, Any, Literal, Union
|
11
14
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
12
15
|
|
13
16
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
|
@@ -17,11 +20,13 @@ from typing_extensions import Self
|
|
17
20
|
|
18
21
|
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner, Options
|
19
22
|
from .__types import DictData, DictStr
|
20
|
-
from .conf import
|
23
|
+
from .conf import FileLoad
|
24
|
+
|
25
|
+
Interval = Literal["daily", "weekly", "monthly"]
|
21
26
|
|
22
27
|
|
23
28
|
def interval2crontab(
|
24
|
-
interval:
|
29
|
+
interval: Interval,
|
25
30
|
*,
|
26
31
|
day: str | None = None,
|
27
32
|
time: str = "00:00",
|
@@ -59,10 +64,11 @@ def interval2crontab(
|
|
59
64
|
|
60
65
|
|
61
66
|
class On(BaseModel):
|
62
|
-
"""On
|
67
|
+
"""On model (Warped crontab object by Pydantic model) to keep crontab value
|
68
|
+
and generate CronRunner object from this crontab value.
|
63
69
|
|
64
|
-
|
65
|
-
|
70
|
+
Methods:
|
71
|
+
- generate: is the main use-case of this schedule object.
|
66
72
|
"""
|
67
73
|
|
68
74
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
@@ -90,40 +96,48 @@ class On(BaseModel):
|
|
90
96
|
description="A timezone string value",
|
91
97
|
alias="timezone",
|
92
98
|
),
|
93
|
-
] = "
|
99
|
+
] = "UTC"
|
94
100
|
|
95
101
|
@classmethod
|
96
102
|
def from_value(cls, value: DictStr, extras: DictData) -> Self:
|
97
103
|
"""Constructor from values that will generate crontab by function.
|
98
104
|
|
99
|
-
:param value: A mapping value that will generate crontab
|
100
|
-
schedule model.
|
101
|
-
:param extras: An
|
105
|
+
:param value: (DictStr) A mapping value that will generate crontab
|
106
|
+
before create schedule model.
|
107
|
+
:param extras: (DictData) An extra parameter that use to override core
|
108
|
+
config value.
|
102
109
|
"""
|
103
110
|
passing: DictStr = {}
|
111
|
+
|
104
112
|
if "timezone" in value:
|
105
113
|
passing["tz"] = value.pop("timezone")
|
114
|
+
elif "tz" in value:
|
115
|
+
passing["tz"] = value.pop("tz")
|
116
|
+
|
106
117
|
passing["cronjob"] = interval2crontab(
|
107
118
|
**{v: value[v] for v in value if v in ("interval", "day", "time")}
|
108
119
|
)
|
120
|
+
print(passing)
|
109
121
|
return cls(extras=extras | passing.pop("extras", {}), **passing)
|
110
122
|
|
111
123
|
@classmethod
|
112
124
|
def from_conf(
|
113
125
|
cls,
|
114
126
|
name: str,
|
127
|
+
*,
|
115
128
|
extras: DictData | None = None,
|
116
129
|
) -> Self:
|
117
|
-
"""Constructor from the name of config that will use loader
|
118
|
-
getting the data.
|
130
|
+
"""Constructor from the name of config loader that will use loader
|
131
|
+
object for getting the `On` data.
|
119
132
|
|
120
|
-
:param name: A name of config that will get from loader.
|
121
|
-
:param extras: An extra parameter that
|
133
|
+
:param name: (str) A name of config that will get from loader.
|
134
|
+
:param extras: (DictData) An extra parameter that use to override core
|
135
|
+
config values.
|
122
136
|
|
123
137
|
:rtype: Self
|
124
138
|
"""
|
125
139
|
extras: DictData = extras or {}
|
126
|
-
loader:
|
140
|
+
loader: FileLoad = FileLoad(name, extras=extras)
|
127
141
|
|
128
142
|
# NOTE: Validate the config type match with current connection model
|
129
143
|
if loader.type != cls.__name__:
|
@@ -155,17 +169,17 @@ class On(BaseModel):
|
|
155
169
|
)
|
156
170
|
|
157
171
|
@model_validator(mode="before")
|
158
|
-
def __prepare_values(cls,
|
172
|
+
def __prepare_values(cls, data: Any) -> Any:
|
159
173
|
"""Extract tz key from value and change name to timezone key.
|
160
174
|
|
161
|
-
:param
|
175
|
+
:param data: (DictData) A data that want to pass for create an On
|
162
176
|
model.
|
163
177
|
|
164
178
|
:rtype: DictData
|
165
179
|
"""
|
166
|
-
if tz :=
|
167
|
-
|
168
|
-
return
|
180
|
+
if isinstance(data, dict) and (tz := data.pop("tz", None)):
|
181
|
+
data["timezone"] = tz
|
182
|
+
return data
|
169
183
|
|
170
184
|
@field_validator("tz")
|
171
185
|
def __validate_tz(cls, value: str) -> str:
|
@@ -238,6 +252,9 @@ class On(BaseModel):
|
|
238
252
|
"""Return a next datetime from Cron runner object that start with any
|
239
253
|
date that given from input.
|
240
254
|
|
255
|
+
:param start: (str | datetime) A start datetime that use to generate
|
256
|
+
the CronRunner object.
|
257
|
+
|
241
258
|
:rtype: CronRunner
|
242
259
|
"""
|
243
260
|
runner: CronRunner = self.generate(start=start)
|