ddeutil-workflow 0.0.4__py3-none-any.whl → 0.0.5__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.4"
1
+ __version__: str = "0.0.5"
@@ -7,10 +7,10 @@ from __future__ import annotations
7
7
 
8
8
  import copy
9
9
  from collections.abc import Iterator
10
+ from dataclasses import dataclass, field
10
11
  from datetime import datetime, timedelta, timezone
11
12
  from functools import partial, total_ordering
12
13
  from typing import (
13
- Any,
14
14
  Callable,
15
15
  Optional,
16
16
  Union,
@@ -37,31 +37,55 @@ WEEKDAYS: dict[str, int] = {
37
37
  "Sat": 6,
38
38
  }
39
39
 
40
- CRON_UNITS: tuple[dict[str, Any], ...] = (
41
- {
42
- "name": "minute",
43
- "range": partial(range, 0, 60),
44
- "min": 0,
45
- "max": 59,
46
- },
47
- {
48
- "name": "hour",
49
- "range": partial(range, 0, 24),
50
- "min": 0,
51
- "max": 23,
52
- },
53
- {
54
- "name": "day",
55
- "range": partial(range, 1, 32),
56
- "min": 1,
57
- "max": 31,
58
- },
59
- {
60
- "name": "month",
61
- "range": partial(range, 1, 13),
62
- "min": 1,
63
- "max": 12,
64
- "alt": [
40
+
41
+ @dataclass(frozen=True)
42
+ class Unit:
43
+ name: str
44
+ range: partial
45
+ min: int
46
+ max: int
47
+ alt: list[str] = field(default_factory=list)
48
+
49
+ def __repr__(self) -> str:
50
+ return (
51
+ f"{self.__class__}(name={self.name!r}, range={self.range},"
52
+ f"min={self.min}, max={self.max}"
53
+ f"{f', alt={self.alt}' if self.alt else ''})"
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class Options:
59
+ output_weekday_names: bool = False
60
+ output_month_names: bool = False
61
+ output_hashes: bool = False
62
+
63
+
64
+ CRON_UNITS: tuple[Unit, ...] = (
65
+ Unit(
66
+ name="minute",
67
+ range=partial(range, 0, 60),
68
+ min=0,
69
+ max=59,
70
+ ),
71
+ Unit(
72
+ name="hour",
73
+ range=partial(range, 0, 24),
74
+ min=0,
75
+ max=23,
76
+ ),
77
+ Unit(
78
+ name="day",
79
+ range=partial(range, 1, 32),
80
+ min=1,
81
+ max=31,
82
+ ),
83
+ Unit(
84
+ name="month",
85
+ range=partial(range, 1, 13),
86
+ min=1,
87
+ max=12,
88
+ alt=[
65
89
  "JAN",
66
90
  "FEB",
67
91
  "MAR",
@@ -75,13 +99,13 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
75
99
  "NOV",
76
100
  "DEC",
77
101
  ],
78
- },
79
- {
80
- "name": "weekday",
81
- "range": partial(range, 0, 7),
82
- "min": 0,
83
- "max": 6,
84
- "alt": [
102
+ ),
103
+ Unit(
104
+ name="weekday",
105
+ range=partial(range, 0, 7),
106
+ min=0,
107
+ max=6,
108
+ alt=[
85
109
  "SUN",
86
110
  "MON",
87
111
  "TUE",
@@ -90,16 +114,16 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
90
114
  "FRI",
91
115
  "SAT",
92
116
  ],
93
- },
117
+ ),
94
118
  )
95
119
 
96
- CRON_UNITS_AWS: tuple = CRON_UNITS + (
97
- {
98
- "name": "year",
99
- "range": partial(range, 1990, 2101),
100
- "min": 1990,
101
- "max": 2100,
102
- },
120
+ CRON_UNITS_AWS: tuple[Unit, ...] = CRON_UNITS + (
121
+ Unit(
122
+ name="year",
123
+ range=partial(range, 1990, 2101),
124
+ min=1990,
125
+ max=2100,
126
+ ),
103
127
  )
104
128
 
105
129
 
@@ -115,31 +139,54 @@ class CronPart:
115
139
 
116
140
  def __init__(
117
141
  self,
118
- unit: dict,
119
- values: Union[str, list[int]],
120
- options: dict,
121
- ):
122
- self.unit: dict = unit
123
- self.options: dict = options
142
+ unit: Unit,
143
+ values: str | list[int],
144
+ options: Options,
145
+ ) -> None:
146
+ self.unit: Unit = unit
147
+ self.options: Options = options
148
+
124
149
  if isinstance(values, str):
125
150
  values: list[int] = self.from_str(values) if values != "?" else []
126
151
  elif isinstance_check(values, list[int]):
127
152
  values: list[int] = self.replace_weekday(values)
128
153
  else:
129
154
  raise TypeError(f"Invalid type of value in cron part: {values}.")
130
- unique_values: list[int] = self.out_of_range(
155
+ self.values: list[int] = self.out_of_range(
131
156
  sorted(dict.fromkeys(values))
132
157
  )
133
- self.values: list[int] = unique_values
134
158
 
135
159
  def __str__(self) -> str:
136
- """Return str that use output to ``self.to_str()`` method."""
137
- return self.to_str()
160
+ """Generate String value from part of cronjob."""
161
+ _hash: str = "H" if self.options.output_hashes else "*"
162
+
163
+ if self.is_full:
164
+ return _hash
165
+
166
+ if self.is_interval:
167
+ if self.is_full_interval:
168
+ return f"{_hash}/{self.step}"
169
+ _hash: str = (
170
+ f"H({self.filler(self.min)}-{self.filler(self.max)})"
171
+ if _hash == "H"
172
+ else f"{self.filler(self.min)}-{self.filler(self.max)}"
173
+ )
174
+ return f"{_hash}/{self.step}"
175
+
176
+ cron_range_strings: list[str] = []
177
+ for cron_range in self.ranges():
178
+ if isinstance(cron_range, list):
179
+ cron_range_strings.append(
180
+ f"{self.filler(cron_range[0])}-{self.filler(cron_range[1])}"
181
+ )
182
+ else:
183
+ cron_range_strings.append(f"{self.filler(cron_range)}")
184
+ return ",".join(cron_range_strings) if cron_range_strings else "?"
138
185
 
139
- def __repr__(self):
186
+ def __repr__(self) -> str:
140
187
  return (
141
188
  f"{self.__class__.__name__}"
142
- f"(unit={self.unit}, values={self.to_str()!r})"
189
+ f"(unit={self.unit}, values={self.__str__()!r})"
143
190
  )
144
191
 
145
192
  def __lt__(self, other) -> bool:
@@ -148,10 +195,6 @@ class CronPart:
148
195
  def __eq__(self, other) -> bool:
149
196
  return self.values == other.values
150
197
 
151
- @property
152
- def is_weekday(self) -> bool:
153
- return self.unit["name"] == "weekday"
154
-
155
198
  @property
156
199
  def min(self) -> int:
157
200
  """Returns the smallest value in the range."""
@@ -176,15 +219,13 @@ class CronPart:
176
219
  @property
177
220
  def is_full(self) -> bool:
178
221
  """Returns true if range has all the values of the unit."""
179
- return len(self.values) == (
180
- self.unit.get("max") - self.unit.get("min") + 1
181
- )
222
+ return len(self.values) == (self.unit.max - self.unit.min + 1)
182
223
 
183
224
  def from_str(self, value: str) -> tuple[int, ...]:
184
225
  """Parses a string as a range of positive integers. The string should
185
226
  include only `-` and `,` special strings.
186
227
 
187
- :param value: a string value
228
+ :param value: A string value
188
229
  :type value: str
189
230
 
190
231
  TODO: support for `L`, `W`, and `#`
@@ -247,7 +288,7 @@ class CronPart:
247
288
  if (value_step and not is_int(value_step)) or value_step == "":
248
289
  raise ValueError(
249
290
  f"Invalid interval step value {value_step!r} for "
250
- f'{self.unit["name"]!r}'
291
+ f"{self.unit.name!r}"
251
292
  )
252
293
 
253
294
  interval_list.append(self._interval(value_range_list, value_step))
@@ -255,16 +296,14 @@ class CronPart:
255
296
 
256
297
  def replace_alternative(self, value: str) -> str:
257
298
  """Replaces the alternative representations of numbers in a string."""
258
- for i, alt in enumerate(self.unit.get("alt", [])):
299
+ for i, alt in enumerate(self.unit.alt):
259
300
  if alt in value:
260
- value: str = value.replace(alt, str(self.unit["min"] + i))
301
+ value: str = value.replace(alt, str(self.unit.min + i))
261
302
  return value
262
303
 
263
- def replace_weekday(
264
- self, values: Union[list[int], Iterator[int]]
265
- ) -> list[int]:
304
+ def replace_weekday(self, values: list[int] | Iterator[int]) -> list[int]:
266
305
  """Replaces all 7 with 0 as Sunday can be represented by both."""
267
- if self.is_weekday:
306
+ if self.unit.name == "weekday":
268
307
  return [0 if value == 7 else value for value in values]
269
308
  return list(values)
270
309
 
@@ -277,20 +316,20 @@ class CronPart:
277
316
  :rtype: list[int]
278
317
  """
279
318
  if values:
280
- if (first := values[0]) < self.unit["min"]:
319
+ if (first := values[0]) < self.unit.min:
281
320
  raise ValueError(
282
- f'Value {first!r} out of range for {self.unit["name"]!r}'
321
+ f"Value {first!r} out of range for {self.unit.name!r}"
283
322
  )
284
- elif (last := values[-1]) > self.unit["max"]:
323
+ elif (last := values[-1]) > self.unit.max:
285
324
  raise ValueError(
286
- f'Value {last!r} out of range for {self.unit["name"]!r}'
325
+ f"Value {last!r} out of range for {self.unit.name!r}"
287
326
  )
288
327
  return values
289
328
 
290
329
  def _parse_range(self, value: str) -> list[int]:
291
330
  """Parses a range string."""
292
331
  if value == "*":
293
- return list(self.unit["range"]())
332
+ return list(self.unit.range())
294
333
  elif value.count("-") > 1:
295
334
  raise ValueError(f"Invalid value {value}")
296
335
  try:
@@ -306,7 +345,9 @@ class CronPart:
306
345
  return self.replace_weekday(sub_parts)
307
346
 
308
347
  def _interval(
309
- self, values: list[int], step: Optional[int] = None
348
+ self,
349
+ values: list[int],
350
+ step: int | None = None,
310
351
  ) -> list[int]:
311
352
  """Applies an interval step to a collection of values."""
312
353
  if not step:
@@ -314,7 +355,7 @@ class CronPart:
314
355
  elif (_step := int(step)) < 1:
315
356
  raise ValueError(
316
357
  f"Invalid interval step value {_step!r} for "
317
- f'{self.unit["name"]!r}'
358
+ f"{self.unit.name!r}"
318
359
  )
319
360
  min_value: int = values[0]
320
361
  return [
@@ -340,8 +381,8 @@ class CronPart:
340
381
  """Returns true if the range contains all the interval values."""
341
382
  if step := self.step:
342
383
  return (
343
- self.min == self.unit["min"]
344
- and (self.max + step) > self.unit["max"]
384
+ self.min == self.unit.min
385
+ and (self.max + step) > self.unit.max
345
386
  and (
346
387
  len(self.values)
347
388
  == (round((self.max - self.min) / step) + 1)
@@ -376,33 +417,6 @@ class CronPart:
376
417
  start_number: Optional[int] = value
377
418
  return multi_dim_values
378
419
 
379
- def to_str(self) -> str:
380
- """Returns the cron range as a string value."""
381
- _hash: str = "H" if self.options.get("output_hashes") else "*"
382
-
383
- if self.is_full:
384
- return _hash
385
-
386
- if self.is_interval:
387
- if self.is_full_interval:
388
- return f"{_hash}/{self.step}"
389
- _hash: str = (
390
- f"H({self.filler(self.min)}-{self.filler(self.max)})"
391
- if _hash == "H"
392
- else f"{self.filler(self.min)}-{self.filler(self.max)}"
393
- )
394
- return f"{_hash}/{self.step}"
395
-
396
- cron_range_strings: list[str] = []
397
- for cron_range in self.ranges():
398
- if isinstance(cron_range, list):
399
- cron_range_strings.append(
400
- f"{self.filler(cron_range[0])}-{self.filler(cron_range[1])}"
401
- )
402
- else:
403
- cron_range_strings.append(f"{self.filler(cron_range)}")
404
- return ",".join(cron_range_strings) if cron_range_strings else "?"
405
-
406
420
  def filler(self, value: int) -> int | str:
407
421
  """Formats weekday and month names as string when the relevant options
408
422
  are set.
@@ -413,15 +427,15 @@ class CronPart:
413
427
  :rtype: int | str
414
428
  """
415
429
  return (
416
- self.unit["alt"][value - self.unit["min"]]
430
+ self.unit.alt[value - self.unit.min]
417
431
  if (
418
432
  (
419
- self.options["output_weekday_names"]
420
- and self.unit["name"] == "weekday"
433
+ self.options.output_weekday_names
434
+ and self.unit.name == "weekday"
421
435
  )
422
436
  or (
423
- self.options["output_month_names"]
424
- and self.unit["name"] == "month"
437
+ self.options.output_month_names
438
+ and self.unit.name == "month"
425
439
  )
426
440
  )
427
441
  else value
@@ -433,7 +447,7 @@ class CronJob:
433
447
  """The Cron Job Converter object that generate datetime dimension of cron
434
448
  job schedule format,
435
449
 
436
- * * * * * <command to execute>
450
+ ... * * * * * <command to execute>
437
451
 
438
452
  (i) minute (0 - 59)
439
453
  (ii) hour (0 - 23)
@@ -447,25 +461,20 @@ class CronJob:
447
461
  Support special value with `/`, `*`, `-`, `,`, and `?` (in day of month
448
462
  and day of week value).
449
463
 
450
- :ref:
464
+ References:
451
465
  - https://github.com/Sonic0/cron-converter
452
466
  - https://pypi.org/project/python-crontab/
453
467
  """
454
468
 
455
469
  cron_length: int = 5
456
-
457
- options_defaults: dict[str, bool] = {
458
- "output_weekday_names": False,
459
- "output_month_names": False,
460
- "output_hashes": False,
461
- }
470
+ cron_units: tuple[Unit, ...] = CRON_UNITS
462
471
 
463
472
  def __init__(
464
473
  self,
465
474
  value: Union[list[list[int]], str],
466
475
  *,
467
476
  option: Optional[dict[str, bool]] = None,
468
- ):
477
+ ) -> None:
469
478
  if isinstance(value, str):
470
479
  value: list[str] = value.strip().split()
471
480
  elif not isinstance_check(value, list[list[int]]):
@@ -473,16 +482,22 @@ class CronJob:
473
482
  f"{self.__class__.__name__} cron value does not support "
474
483
  f"type: {type(value)}."
475
484
  )
485
+
486
+ # NOTE: Validate length of crontab of this class.
476
487
  if len(value) != self.cron_length:
477
488
  raise ValueError(
478
489
  f"Invalid cron value does not have length equal "
479
490
  f"{self.cron_length}: {value}."
480
491
  )
481
- self._options: dict[str, bool] = self.options_defaults | (option or {})
482
- self._parts: list[CronPart] = [
483
- CronPart(unit, values=item, options=self._options)
484
- for item, unit in zip(value, CRON_UNITS)
492
+ self.options: Options = Options(**(option or {}))
493
+
494
+ # NOTE: Start initial crontab for each part
495
+ self.parts: list[CronPart] = [
496
+ CronPart(unit, values=item, options=self.options)
497
+ for item, unit in zip(value, self.cron_units)
485
498
  ]
499
+
500
+ # NOTE: Validate values of `day` and `dow` from parts.
486
501
  if self.day == self.dow == []:
487
502
  raise ValueError(
488
503
  "Invalid cron value when set the `?` on day of month and "
@@ -490,12 +505,13 @@ class CronJob:
490
505
  )
491
506
 
492
507
  def __str__(self) -> str:
493
- return " ".join(str(part) for part in self._parts)
508
+ """Return joining with space of each value in parts."""
509
+ return " ".join(str(part) for part in self.parts)
494
510
 
495
511
  def __repr__(self) -> str:
496
512
  return (
497
513
  f"{self.__class__.__name__}(value={self.__str__()!r}, "
498
- f"option={self._options})"
514
+ f"option={self.options.__dict__})"
499
515
  )
500
516
 
501
517
  def __lt__(self, other) -> bool:
@@ -510,10 +526,6 @@ class CronJob:
510
526
  for part, other_part in zip(self.parts, other.parts)
511
527
  )
512
528
 
513
- @property
514
- def parts(self) -> list[CronPart]:
515
- return self._parts
516
-
517
529
  @property
518
530
  def parts_order(self) -> Iterator[CronPart]:
519
531
  return reversed(self.parts[:3] + [self.parts[4], self.parts[3]])
@@ -521,31 +533,31 @@ class CronJob:
521
533
  @property
522
534
  def minute(self):
523
535
  """Return part of minute."""
524
- return self._parts[0]
536
+ return self.parts[0]
525
537
 
526
538
  @property
527
539
  def hour(self):
528
540
  """Return part of hour."""
529
- return self._parts[1]
541
+ return self.parts[1]
530
542
 
531
543
  @property
532
544
  def day(self):
533
545
  """Return part of day."""
534
- return self._parts[2]
546
+ return self.parts[2]
535
547
 
536
548
  @property
537
549
  def month(self):
538
550
  """Return part of month."""
539
- return self._parts[3]
551
+ return self.parts[3]
540
552
 
541
553
  @property
542
554
  def dow(self):
543
555
  """Return part of day of month."""
544
- return self._parts[4]
556
+ return self.parts[4]
545
557
 
546
558
  def to_list(self) -> list[list[int]]:
547
559
  """Returns the cron schedule as a 2-dimensional list of integers."""
548
- return [part.values for part in self._parts]
560
+ return [part.values for part in self.parts]
549
561
 
550
562
  def schedule(
551
563
  self, date: Optional[datetime] = None, _tz: Optional[str] = None
@@ -554,6 +566,11 @@ class CronJob:
554
566
  return CronRunner(self, date, tz_str=_tz)
555
567
 
556
568
 
569
+ class CronJobAWS(CronJob):
570
+ cron_length = 6
571
+ cron_units = CRON_UNITS_AWS
572
+
573
+
557
574
  class CronRunner:
558
575
  """Create an instance of Date Runner object for datetime generate with
559
576
  cron schedule object value.
@@ -661,7 +678,8 @@ class CronRunner:
661
678
  return False
662
679
 
663
680
 
664
- __all__: tuple[str, ...] = (
681
+ __all__ = (
665
682
  "CronJob",
666
683
  "CronRunner",
684
+ "WEEKDAYS",
667
685
  )
@@ -37,6 +37,9 @@ class Engine(BaseModel):
37
37
 
38
38
  @model_validator(mode="before")
39
39
  def __prepare_registry(cls, values: DictData) -> DictData:
40
+ """Prepare registry value that passing with string type. It convert the
41
+ string type to list of string.
42
+ """
40
43
  if (_regis := values.get("registry")) and isinstance(_regis, str):
41
44
  values["registry"] = [_regis]
42
45
  return values
@@ -83,7 +86,9 @@ class SimLoad:
83
86
 
84
87
  @cached_property
85
88
  def type(self) -> BaseModelType:
86
- """Return object type which implement in `config_object` key."""
89
+ """Return object of string type which implement on any registry. The
90
+ object type
91
+ """
87
92
  if not (_typ := self.data.get("type")):
88
93
  raise ValueError(
89
94
  f"the 'type' value: {_typ} does not exists in config data."
@@ -100,6 +105,9 @@ class SimLoad:
100
105
  return import_string(f"{_typ}")
101
106
 
102
107
  def load(self) -> AnyModel:
108
+ """Parsing config data to the object type for initialize with model
109
+ validate method.
110
+ """
103
111
  return self.type.model_validate(self.data)
104
112
 
105
113