ddeutil-workflow 0.0.4__py3-none-any.whl → 0.0.6__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.
@@ -7,14 +7,10 @@ from __future__ import annotations
7
7
 
8
8
  import copy
9
9
  from collections.abc import Iterator
10
- from datetime import datetime, timedelta, timezone
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
11
12
  from functools import partial, total_ordering
12
- from typing import (
13
- Any,
14
- Callable,
15
- Optional,
16
- Union,
17
- )
13
+ from typing import Callable, Optional, Union
18
14
  from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
19
15
 
20
16
  from ddeutil.core import (
@@ -37,31 +33,55 @@ WEEKDAYS: dict[str, int] = {
37
33
  "Sat": 6,
38
34
  }
39
35
 
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": [
36
+
37
+ @dataclass(frozen=True)
38
+ class Unit:
39
+ name: str
40
+ range: partial
41
+ min: int
42
+ max: int
43
+ alt: list[str] = field(default_factory=list)
44
+
45
+ def __repr__(self) -> str:
46
+ return (
47
+ f"{self.__class__}(name={self.name!r}, range={self.range},"
48
+ f"min={self.min}, max={self.max}"
49
+ f"{f', alt={self.alt}' if self.alt else ''})"
50
+ )
51
+
52
+
53
+ @dataclass
54
+ class Options:
55
+ output_weekday_names: bool = False
56
+ output_month_names: bool = False
57
+ output_hashes: bool = False
58
+
59
+
60
+ CRON_UNITS: tuple[Unit, ...] = (
61
+ Unit(
62
+ name="minute",
63
+ range=partial(range, 0, 60),
64
+ min=0,
65
+ max=59,
66
+ ),
67
+ Unit(
68
+ name="hour",
69
+ range=partial(range, 0, 24),
70
+ min=0,
71
+ max=23,
72
+ ),
73
+ Unit(
74
+ name="day",
75
+ range=partial(range, 1, 32),
76
+ min=1,
77
+ max=31,
78
+ ),
79
+ Unit(
80
+ name="month",
81
+ range=partial(range, 1, 13),
82
+ min=1,
83
+ max=12,
84
+ alt=[
65
85
  "JAN",
66
86
  "FEB",
67
87
  "MAR",
@@ -75,13 +95,13 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
75
95
  "NOV",
76
96
  "DEC",
77
97
  ],
78
- },
79
- {
80
- "name": "weekday",
81
- "range": partial(range, 0, 7),
82
- "min": 0,
83
- "max": 6,
84
- "alt": [
98
+ ),
99
+ Unit(
100
+ name="weekday",
101
+ range=partial(range, 0, 7),
102
+ min=0,
103
+ max=6,
104
+ alt=[
85
105
  "SUN",
86
106
  "MON",
87
107
  "TUE",
@@ -90,16 +110,16 @@ CRON_UNITS: tuple[dict[str, Any], ...] = (
90
110
  "FRI",
91
111
  "SAT",
92
112
  ],
93
- },
113
+ ),
94
114
  )
95
115
 
96
- CRON_UNITS_AWS: tuple = CRON_UNITS + (
97
- {
98
- "name": "year",
99
- "range": partial(range, 1990, 2101),
100
- "min": 1990,
101
- "max": 2100,
102
- },
116
+ CRON_UNITS_YEAR: tuple[Unit, ...] = CRON_UNITS + (
117
+ Unit(
118
+ name="year",
119
+ range=partial(range, 1990, 2101),
120
+ min=1990,
121
+ max=2100,
122
+ ),
103
123
  )
104
124
 
105
125
 
@@ -115,42 +135,68 @@ class CronPart:
115
135
 
116
136
  def __init__(
117
137
  self,
118
- unit: dict,
119
- values: Union[str, list[int]],
120
- options: dict,
121
- ):
122
- self.unit: dict = unit
123
- self.options: dict = options
138
+ unit: Unit,
139
+ values: str | list[int],
140
+ options: Options,
141
+ ) -> None:
142
+ self.unit: Unit = unit
143
+ self.options: Options = options
144
+
124
145
  if isinstance(values, str):
125
146
  values: list[int] = self.from_str(values) if values != "?" else []
126
147
  elif isinstance_check(values, list[int]):
127
148
  values: list[int] = self.replace_weekday(values)
128
149
  else:
129
150
  raise TypeError(f"Invalid type of value in cron part: {values}.")
130
- unique_values: list[int] = self.out_of_range(
151
+
152
+ self.values: list[int] = self.out_of_range(
131
153
  sorted(dict.fromkeys(values))
132
154
  )
133
- self.values: list[int] = unique_values
134
155
 
135
156
  def __str__(self) -> str:
136
- """Return str that use output to ``self.to_str()`` method."""
137
- return self.to_str()
157
+ """Generate String value from part of cronjob."""
158
+ _hash: str = "H" if self.options.output_hashes else "*"
159
+
160
+ if self.is_full:
161
+ return _hash
162
+
163
+ if self.is_interval:
164
+ if self.is_full_interval:
165
+ return f"{_hash}/{self.step}"
166
+ _hash: str = (
167
+ f"H({self.filler(self.min)}-{self.filler(self.max)})"
168
+ if _hash == "H"
169
+ else f"{self.filler(self.min)}-{self.filler(self.max)}"
170
+ )
171
+ return f"{_hash}/{self.step}"
172
+
173
+ cron_range_strings: list[str] = []
174
+ for cron_range in self.ranges():
175
+ if isinstance(cron_range, list):
176
+ cron_range_strings.append(
177
+ f"{self.filler(cron_range[0])}-{self.filler(cron_range[1])}"
178
+ )
179
+ else:
180
+ cron_range_strings.append(f"{self.filler(cron_range)}")
181
+ return ",".join(cron_range_strings) if cron_range_strings else "?"
138
182
 
139
- def __repr__(self):
183
+ def __repr__(self) -> str:
140
184
  return (
141
185
  f"{self.__class__.__name__}"
142
- f"(unit={self.unit}, values={self.to_str()!r})"
186
+ f"(unit={self.unit}, values={self.__str__()!r})"
143
187
  )
144
188
 
145
189
  def __lt__(self, other) -> bool:
146
- return self.values < other.values
190
+ if isinstance(other, CronPart):
191
+ return self.values < other.values
192
+ elif isinstance(other, list):
193
+ return self.values < other
147
194
 
148
195
  def __eq__(self, other) -> bool:
149
- return self.values == other.values
150
-
151
- @property
152
- def is_weekday(self) -> bool:
153
- return self.unit["name"] == "weekday"
196
+ if isinstance(other, CronPart):
197
+ return self.values == other.values
198
+ elif isinstance(other, list):
199
+ return self.values == other
154
200
 
155
201
  @property
156
202
  def min(self) -> int:
@@ -176,19 +222,17 @@ class CronPart:
176
222
  @property
177
223
  def is_full(self) -> bool:
178
224
  """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
- )
225
+ return len(self.values) == (self.unit.max - self.unit.min + 1)
182
226
 
183
227
  def from_str(self, value: str) -> tuple[int, ...]:
184
228
  """Parses a string as a range of positive integers. The string should
185
229
  include only `-` and `,` special strings.
186
230
 
187
- :param value: a string value
231
+ :param value: A string value that want to parse
188
232
  :type value: str
189
233
 
190
234
  TODO: support for `L`, `W`, and `#`
191
- TODO: if you didn't care what day of the week the 7th was, you
235
+ TODO: if you didn't care what day of the week the 7th was, you
192
236
  could enter ? in the Day-of-week field.
193
237
  TODO: L : the Day-of-month or Day-of-week fields specifies the last day
194
238
  of the month or week.
@@ -198,7 +242,7 @@ class CronPart:
198
242
  TODO: # : 3#2 would be the second Tuesday of the month,
199
243
  the 3 refers to Tuesday because it is the third day of each week.
200
244
 
201
- Examples:
245
+ Noted:
202
246
  - 0 10 * * ? *
203
247
  Run at 10:00 am (UTC) every day
204
248
 
@@ -247,7 +291,7 @@ class CronPart:
247
291
  if (value_step and not is_int(value_step)) or value_step == "":
248
292
  raise ValueError(
249
293
  f"Invalid interval step value {value_step!r} for "
250
- f'{self.unit["name"]!r}'
294
+ f"{self.unit.name!r}"
251
295
  )
252
296
 
253
297
  interval_list.append(self._interval(value_range_list, value_step))
@@ -255,17 +299,20 @@ class CronPart:
255
299
 
256
300
  def replace_alternative(self, value: str) -> str:
257
301
  """Replaces the alternative representations of numbers in a string."""
258
- for i, alt in enumerate(self.unit.get("alt", [])):
302
+ for i, alt in enumerate(self.unit.alt):
259
303
  if alt in value:
260
- value: str = value.replace(alt, str(self.unit["min"] + i))
304
+ value: str = value.replace(alt, str(self.unit.min + i))
261
305
  return value
262
306
 
263
- def replace_weekday(
264
- self, values: Union[list[int], Iterator[int]]
265
- ) -> list[int]:
266
- """Replaces all 7 with 0 as Sunday can be represented by both."""
267
- if self.is_weekday:
268
- return [0 if value == 7 else value for value in values]
307
+ def replace_weekday(self, values: list[int] | Iterator[int]) -> list[int]:
308
+ """Replaces all 7 with 0 as Sunday can be represented by both.
309
+
310
+ :param values: list or iter of int that want to mode by 7
311
+ :rtype: list[int]
312
+ """
313
+ if self.unit.name == "weekday":
314
+ # NOTE: change weekday value in range 0-6 (div-mod by 7).
315
+ return [value % 7 for value in values]
269
316
  return list(values)
270
317
 
271
318
  def out_of_range(self, values: list[int]) -> list[int]:
@@ -277,20 +324,20 @@ class CronPart:
277
324
  :rtype: list[int]
278
325
  """
279
326
  if values:
280
- if (first := values[0]) < self.unit["min"]:
327
+ if (first := values[0]) < self.unit.min:
281
328
  raise ValueError(
282
- f'Value {first!r} out of range for {self.unit["name"]!r}'
329
+ f"Value {first!r} out of range for {self.unit.name!r}"
283
330
  )
284
- elif (last := values[-1]) > self.unit["max"]:
331
+ elif (last := values[-1]) > self.unit.max:
285
332
  raise ValueError(
286
- f'Value {last!r} out of range for {self.unit["name"]!r}'
333
+ f"Value {last!r} out of range for {self.unit.name!r}"
287
334
  )
288
335
  return values
289
336
 
290
337
  def _parse_range(self, value: str) -> list[int]:
291
338
  """Parses a range string."""
292
339
  if value == "*":
293
- return list(self.unit["range"]())
340
+ return list(self.unit.range())
294
341
  elif value.count("-") > 1:
295
342
  raise ValueError(f"Invalid value {value}")
296
343
  try:
@@ -306,7 +353,9 @@ class CronPart:
306
353
  return self.replace_weekday(sub_parts)
307
354
 
308
355
  def _interval(
309
- self, values: list[int], step: Optional[int] = None
356
+ self,
357
+ values: list[int],
358
+ step: int | None = None,
310
359
  ) -> list[int]:
311
360
  """Applies an interval step to a collection of values."""
312
361
  if not step:
@@ -314,7 +363,7 @@ class CronPart:
314
363
  elif (_step := int(step)) < 1:
315
364
  raise ValueError(
316
365
  f"Invalid interval step value {_step!r} for "
317
- f'{self.unit["name"]!r}'
366
+ f"{self.unit.name!r}"
318
367
  )
319
368
  min_value: int = values[0]
320
369
  return [
@@ -340,8 +389,8 @@ class CronPart:
340
389
  """Returns true if the range contains all the interval values."""
341
390
  if step := self.step:
342
391
  return (
343
- self.min == self.unit["min"]
344
- and (self.max + step) > self.unit["max"]
392
+ self.min == self.unit.min
393
+ and (self.max + step) > self.unit.max
345
394
  and (
346
395
  len(self.values)
347
396
  == (round((self.max - self.min) / step) + 1)
@@ -376,33 +425,6 @@ class CronPart:
376
425
  start_number: Optional[int] = value
377
426
  return multi_dim_values
378
427
 
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
428
  def filler(self, value: int) -> int | str:
407
429
  """Formats weekday and month names as string when the relevant options
408
430
  are set.
@@ -413,15 +435,15 @@ class CronPart:
413
435
  :rtype: int | str
414
436
  """
415
437
  return (
416
- self.unit["alt"][value - self.unit["min"]]
438
+ self.unit.alt[value - self.unit.min]
417
439
  if (
418
440
  (
419
- self.options["output_weekday_names"]
420
- and self.unit["name"] == "weekday"
441
+ self.options.output_weekday_names
442
+ and self.unit.name == "weekday"
421
443
  )
422
444
  or (
423
- self.options["output_month_names"]
424
- and self.unit["name"] == "month"
445
+ self.options.output_month_names
446
+ and self.unit.name == "month"
425
447
  )
426
448
  )
427
449
  else value
@@ -433,7 +455,7 @@ class CronJob:
433
455
  """The Cron Job Converter object that generate datetime dimension of cron
434
456
  job schedule format,
435
457
 
436
- * * * * * <command to execute>
458
+ ... * * * * * <command to execute>
437
459
 
438
460
  (i) minute (0 - 59)
439
461
  (ii) hour (0 - 23)
@@ -447,25 +469,20 @@ class CronJob:
447
469
  Support special value with `/`, `*`, `-`, `,`, and `?` (in day of month
448
470
  and day of week value).
449
471
 
450
- :ref:
472
+ References:
451
473
  - https://github.com/Sonic0/cron-converter
452
474
  - https://pypi.org/project/python-crontab/
453
475
  """
454
476
 
455
477
  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
- }
478
+ cron_units: tuple[Unit, ...] = CRON_UNITS
462
479
 
463
480
  def __init__(
464
481
  self,
465
482
  value: Union[list[list[int]], str],
466
483
  *,
467
484
  option: Optional[dict[str, bool]] = None,
468
- ):
485
+ ) -> None:
469
486
  if isinstance(value, str):
470
487
  value: list[str] = value.strip().split()
471
488
  elif not isinstance_check(value, list[list[int]]):
@@ -473,16 +490,22 @@ class CronJob:
473
490
  f"{self.__class__.__name__} cron value does not support "
474
491
  f"type: {type(value)}."
475
492
  )
493
+
494
+ # NOTE: Validate length of crontab of this class.
476
495
  if len(value) != self.cron_length:
477
496
  raise ValueError(
478
497
  f"Invalid cron value does not have length equal "
479
498
  f"{self.cron_length}: {value}."
480
499
  )
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)
500
+ self.options: Options = Options(**(option or {}))
501
+
502
+ # NOTE: Start initial crontab for each part
503
+ self.parts: list[CronPart] = [
504
+ CronPart(unit, values=item, options=self.options)
505
+ for item, unit in zip(value, self.cron_units)
485
506
  ]
507
+
508
+ # NOTE: Validate values of `day` and `dow` from parts.
486
509
  if self.day == self.dow == []:
487
510
  raise ValueError(
488
511
  "Invalid cron value when set the `?` on day of month and "
@@ -490,12 +513,13 @@ class CronJob:
490
513
  )
491
514
 
492
515
  def __str__(self) -> str:
493
- return " ".join(str(part) for part in self._parts)
516
+ """Return joining with space of each value in parts."""
517
+ return " ".join(str(part) for part in self.parts)
494
518
 
495
519
  def __repr__(self) -> str:
496
520
  return (
497
521
  f"{self.__class__.__name__}(value={self.__str__()!r}, "
498
- f"option={self._options})"
522
+ f"option={self.options.__dict__})"
499
523
  )
500
524
 
501
525
  def __lt__(self, other) -> bool:
@@ -510,48 +534,63 @@ class CronJob:
510
534
  for part, other_part in zip(self.parts, other.parts)
511
535
  )
512
536
 
513
- @property
514
- def parts(self) -> list[CronPart]:
515
- return self._parts
516
-
517
537
  @property
518
538
  def parts_order(self) -> Iterator[CronPart]:
519
539
  return reversed(self.parts[:3] + [self.parts[4], self.parts[3]])
520
540
 
521
541
  @property
522
- def minute(self):
542
+ def minute(self) -> CronPart:
523
543
  """Return part of minute."""
524
- return self._parts[0]
544
+ return self.parts[0]
525
545
 
526
546
  @property
527
- def hour(self):
547
+ def hour(self) -> CronPart:
528
548
  """Return part of hour."""
529
- return self._parts[1]
549
+ return self.parts[1]
530
550
 
531
551
  @property
532
- def day(self):
552
+ def day(self) -> CronPart:
533
553
  """Return part of day."""
534
- return self._parts[2]
554
+ return self.parts[2]
535
555
 
536
556
  @property
537
- def month(self):
557
+ def month(self) -> CronPart:
538
558
  """Return part of month."""
539
- return self._parts[3]
559
+ return self.parts[3]
540
560
 
541
561
  @property
542
- def dow(self):
562
+ def dow(self) -> CronPart:
543
563
  """Return part of day of month."""
544
- return self._parts[4]
564
+ return self.parts[4]
545
565
 
546
566
  def to_list(self) -> list[list[int]]:
547
567
  """Returns the cron schedule as a 2-dimensional list of integers."""
548
- return [part.values for part in self._parts]
568
+ return [part.values for part in self.parts]
549
569
 
550
570
  def schedule(
551
- self, date: Optional[datetime] = None, _tz: Optional[str] = None
571
+ self,
572
+ date: datetime | None = None,
573
+ *,
574
+ tz: str | None = None,
552
575
  ) -> CronRunner:
553
- """Returns the time the schedule would run next."""
554
- return CronRunner(self, date, tz_str=_tz)
576
+ """Returns the schedule datetime runner with this cronjob. It would run
577
+ ``next``, ``prev``, or ``reset`` to generate running date that you want.
578
+
579
+ :param date: An initial date that want to mark as the start point.
580
+ :param tz: A string timezone that want to change on runner.
581
+ :rtype: CronRunner
582
+ """
583
+ return CronRunner(self, date, tz=tz)
584
+
585
+
586
+ class CronJobYear(CronJob):
587
+ cron_length = 6
588
+ cron_units = CRON_UNITS_YEAR
589
+
590
+ @property
591
+ def year(self) -> CronPart:
592
+ """Return part of year."""
593
+ return self.parts[5]
555
594
 
556
595
 
557
596
  class CronRunner:
@@ -569,33 +608,37 @@ class CronRunner:
569
608
 
570
609
  def __init__(
571
610
  self,
572
- cron: CronJob,
573
- date: Optional[datetime] = None,
611
+ cron: CronJob | CronJobYear,
612
+ date: datetime | None = None,
574
613
  *,
575
- tz_str: Optional[str] = None,
614
+ tz: str | None = None,
576
615
  ) -> None:
577
- # NOTE: Prepare date and tz_info
578
- self.tz = timezone.utc
579
- if tz_str:
616
+ # NOTE: Prepare timezone if this value does not set, it will use UTC.
617
+ self.tz: ZoneInfo = ZoneInfo("UTC")
618
+ if tz:
580
619
  try:
581
- self.tz = ZoneInfo(tz_str)
620
+ self.tz = ZoneInfo(tz)
582
621
  except ZoneInfoNotFoundError as err:
583
- raise ValueError(f"Invalid timezone: {tz_str}") from err
622
+ raise ValueError(f"Invalid timezone: {tz}") from err
623
+
624
+ # NOTE: Prepare date
584
625
  if date:
585
626
  if not isinstance(date, datetime):
586
627
  raise ValueError(
587
628
  "Input schedule start time is not a valid datetime object."
588
629
  )
589
- self.tz = date.tzinfo
590
- self.date: datetime = date
630
+ if tz is None:
631
+ self.tz = date.tzinfo
632
+ self.date: datetime = date.astimezone(self.tz)
591
633
  else:
592
634
  self.date: datetime = datetime.now(tz=self.tz)
593
635
 
636
+ # NOTE: Add one minute if the second value more than 0.
594
637
  if self.date.second > 0:
595
- self.date: datetime = self.date + timedelta(minutes=+1)
638
+ self.date: datetime = self.date + timedelta(minutes=1)
596
639
 
597
640
  self.__start_date: datetime = self.date
598
- self.cron: CronJob = cron
641
+ self.cron: CronJob | CronJobYear = cron
599
642
  self.reset_flag: bool = True
600
643
 
601
644
  def reset(self) -> None:
@@ -620,7 +663,11 @@ class CronRunner:
620
663
  return self.find_date(reverse=True)
621
664
 
622
665
  def find_date(self, reverse: bool = False) -> datetime:
623
- """Returns the time the schedule would run by `next` or `prev`."""
666
+ """Returns the time the schedule would run by `next` or `prev`.
667
+
668
+ :param reverse: A reverse flag.
669
+ """
670
+ # NOTE: Set reset flag to false if start any action.
624
671
  self.reset_flag: bool = False
625
672
  for _ in range(25):
626
673
  if all(
@@ -639,7 +686,7 @@ class CronRunner:
639
686
  "minute": "hour",
640
687
  }
641
688
  current_value: int = getattr(self.date, switch[mode])
642
- _addition: Callable[[], bool] = (
689
+ _addition_condition: Callable[[], bool] = (
643
690
  (
644
691
  lambda: WEEKDAYS.get(self.date.strftime("%a"))
645
692
  not in self.cron.dow.values
@@ -647,21 +694,20 @@ class CronRunner:
647
694
  if mode == "day"
648
695
  else lambda: False
649
696
  )
697
+ # NOTE: Start while-loop for checking this date include in this cronjob.
650
698
  while (
651
699
  getattr(self.date, mode) not in getattr(self.cron, mode).values
652
- ) or _addition():
653
- self.date: datetime = next_date(
654
- self.date, mode=mode, reverse=reverse
655
- )
656
- self.date: datetime = replace_date(
657
- self.date, mode=mode, reverse=reverse
658
- )
700
+ ) or _addition_condition():
701
+ self.date: datetime = next_date(self.date, mode, reverse=reverse)
702
+ self.date: datetime = replace_date(self.date, mode, reverse=reverse)
659
703
  if current_value != getattr(self.date, switch[mode]):
660
704
  return mode != "month"
661
705
  return False
662
706
 
663
707
 
664
- __all__: tuple[str, ...] = (
708
+ __all__ = (
665
709
  "CronJob",
710
+ "CronJobYear",
666
711
  "CronRunner",
712
+ "WEEKDAYS",
667
713
  )