Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/cron_descriptor/ExpressionDescriptor.py: 43%
264 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-25 06:11 +0000
1# The MIT License (MIT)
2#
3# Copyright (c) 2016 Adam Schubert
4#
5# Permission is hereby granted, free of charge, to any person obtaining a copy
6# of this software and associated documentation files (the "Software"), to deal
7# in the Software without restriction, including without limitation the rights
8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9# copies of the Software, and to permit persons to whom the Software is
10# furnished to do so, subject to the following conditions:
11#
12# The above copyright notice and this permission notice shall be included in all
13# copies or substantial portions of the Software.
14#
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21# SOFTWARE.
23import re
24import datetime
25import calendar
27from .GetText import GetText
28from .CasingTypeEnum import CasingTypeEnum
29from .DescriptionTypeEnum import DescriptionTypeEnum
30from .ExpressionParser import ExpressionParser
31from .Options import Options
32from .StringBuilder import StringBuilder
33from .Exception import FormatException, WrongArgumentException
36class ExpressionDescriptor:
38 """
39 Converts a Cron Expression into a human readable string
40 """
42 _special_characters = ['/', '-', ',', '*']
44 _expression = ''
45 _options = None
46 _expression_parts = []
47 _parsed = False
49 def __init__(self, expression, options=None, **kwargs):
50 """Initializes a new instance of the ExpressionDescriptor
52 Args:
53 expression: The cron expression string
54 options: Options to control the output description
55 Raises:
56 WrongArgumentException: if kwarg is unknown
58 """
59 if options is None:
60 options = Options()
61 self._expression = expression
62 self._options = options
63 self._expression_parts = []
64 self._parsed = False
66 # if kwargs in _options, overwrite it, if not raise exception
67 for kwarg in kwargs:
68 if hasattr(self._options, kwarg):
69 setattr(self._options, kwarg, kwargs[kwarg])
70 else:
71 raise WrongArgumentException("Unknown {} configuration argument".format(kwarg))
73 # Initializes localization
74 self.get_text = GetText(options.locale_code)
76 def _(self, message):
77 return self.get_text.trans.gettext(message)
79 def get_description(self, description_type=DescriptionTypeEnum.FULL):
80 """Generates a humanreadable string for the Cron Expression
82 Args:
83 description_type: Which part(s) of the expression to describe
84 Returns:
85 The cron expression description
86 Raises:
87 Exception: if throw_exception_on_parse_error is True
89 """
90 try:
91 if self._parsed is False:
92 parser = ExpressionParser(self._expression, self._options)
93 self._expression_parts = parser.parse()
94 self._parsed = True
96 choices = {
97 DescriptionTypeEnum.FULL: self.get_full_description,
98 DescriptionTypeEnum.TIMEOFDAY: self.get_time_of_day_description,
99 DescriptionTypeEnum.HOURS: self.get_hours_description,
100 DescriptionTypeEnum.MINUTES: self.get_minutes_description,
101 DescriptionTypeEnum.SECONDS: self.get_seconds_description,
102 DescriptionTypeEnum.DAYOFMONTH: self.get_day_of_month_description,
103 DescriptionTypeEnum.MONTH: self.get_month_description,
104 DescriptionTypeEnum.DAYOFWEEK: self.get_day_of_week_description,
105 DescriptionTypeEnum.YEAR: self.get_year_description,
106 }
108 description = choices.get(description_type, self.get_seconds_description)()
110 except Exception as ex:
111 if self._options.throw_exception_on_parse_error:
112 raise
113 else:
114 description = str(ex)
115 return description
117 def get_full_description(self):
118 """Generates the FULL description
120 Returns:
121 The FULL description
122 Raises:
123 FormatException: if formatting fails and throw_exception_on_parse_error is True
125 """
127 try:
128 time_segment = self.get_time_of_day_description()
129 day_of_month_desc = self.get_day_of_month_description()
130 month_desc = self.get_month_description()
131 day_of_week_desc = self.get_day_of_week_description()
132 year_desc = self.get_year_description()
134 description = "{0}{1}{2}{3}{4}".format(
135 time_segment,
136 day_of_month_desc,
137 day_of_week_desc,
138 month_desc,
139 year_desc)
141 description = self.transform_verbosity(description, self._options.verbose)
142 description = ExpressionDescriptor.transform_case(description, self._options.casing_type)
143 except Exception:
144 description = self._(
145 "An error occurred when generating the expression description. Check the cron expression syntax."
146 )
147 if self._options.throw_exception_on_parse_error:
148 raise FormatException(description)
150 return description
152 def get_time_of_day_description(self):
153 """Generates a description for only the TIMEOFDAY portion of the expression
155 Returns:
156 The TIMEOFDAY description
158 """
159 seconds_expression = self._expression_parts[0]
160 minute_expression = self._expression_parts[1]
161 hour_expression = self._expression_parts[2]
163 description = StringBuilder()
165 # handle special cases first
166 if any(exp in minute_expression for exp in self._special_characters) is False and \
167 any(exp in hour_expression for exp in self._special_characters) is False and \
168 any(exp in seconds_expression for exp in self._special_characters) is False:
169 # specific time of day (i.e. 10 14)
170 description.append(self._("At "))
171 description.append(
172 self.format_time(
173 hour_expression,
174 minute_expression,
175 seconds_expression))
176 elif seconds_expression == "" and "-" in minute_expression and \
177 "," not in minute_expression and \
178 any(exp in hour_expression for exp in self._special_characters) is False:
179 # minute range in single hour (i.e. 0-10 11)
180 minute_parts = minute_expression.split('-')
181 description.append(self._("Every minute between {0} and {1}").format(
182 self.format_time(hour_expression, minute_parts[0]), self.format_time(hour_expression, minute_parts[1])))
183 elif seconds_expression == "" and "," in hour_expression and "-" not in hour_expression and \
184 any(exp in minute_expression for exp in self._special_characters) is False:
185 # hours list with single minute (o.e. 30 6,14,16)
186 hour_parts = hour_expression.split(',')
187 description.append(self._("At"))
188 for i, hour_part in enumerate(hour_parts):
189 description.append(" ")
190 description.append(self.format_time(hour_part, minute_expression))
192 if i < (len(hour_parts) - 2):
193 description.append(",")
195 if i == len(hour_parts) - 2:
196 description.append(self._(" and"))
197 else:
198 # default time description
199 seconds_description = self.get_seconds_description()
200 minutes_description = self.get_minutes_description()
201 hours_description = self.get_hours_description()
203 description.append(seconds_description)
205 if description and minutes_description:
206 description.append(", ")
208 description.append(minutes_description)
210 if description and hours_description:
211 description.append(", ")
213 description.append(hours_description)
214 return str(description)
216 def get_seconds_description(self):
217 """Generates a description for only the SECONDS portion of the expression
219 Returns:
220 The SECONDS description
222 """
224 def get_description_format(s):
225 if s == "0":
226 return ""
228 try:
229 if int(s) < 20:
230 return self._("at {0} seconds past the minute")
231 else:
232 return self._("at {0} seconds past the minute [grThen20]") or self._("at {0} seconds past the minute")
233 except ValueError:
234 return self._("at {0} seconds past the minute")
236 return self.get_segment_description(
237 self._expression_parts[0],
238 self._("every second"),
239 lambda s: s,
240 lambda s: self._("every {0} seconds").format(s),
241 lambda s: self._("seconds {0} through {1} past the minute"),
242 get_description_format,
243 lambda s: self._(", second {0} through second {1}") or self._(", {0} through {1}")
244 )
246 def get_minutes_description(self):
247 """Generates a description for only the MINUTE portion of the expression
249 Returns:
250 The MINUTE description
252 """
253 seconds_expression = self._expression_parts[0]
255 def get_description_format(s):
256 if s == "0" and seconds_expression == "":
257 return ""
259 try:
260 if int(s) < 20:
261 return self._("at {0} minutes past the hour")
262 else:
263 return self._("at {0} minutes past the hour [grThen20]") or self._("at {0} minutes past the hour")
264 except ValueError:
265 return self._("at {0} minutes past the hour")
267 return self.get_segment_description(
268 self._expression_parts[1],
269 self._("every minute"),
270 lambda s: s,
271 lambda s: self._("every {0} minutes").format(s),
272 lambda s: self._("minutes {0} through {1} past the hour"),
273 get_description_format,
274 lambda s: self._(", minute {0} through minute {1}") or self._(", {0} through {1}")
275 )
277 def get_hours_description(self):
278 """Generates a description for only the HOUR portion of the expression
280 Returns:
281 The HOUR description
283 """
284 expression = self._expression_parts[2]
285 return self.get_segment_description(
286 expression,
287 self._("every hour"),
288 lambda s: self.format_time(s, "0"),
289 lambda s: self._("every {0} hours").format(s),
290 lambda s: self._("between {0} and {1}"),
291 lambda s: self._("at {0}"),
292 lambda s: self._(", hour {0} through hour {1}") or self._(", {0} through {1}")
293 )
295 def get_day_of_week_description(self):
296 """Generates a description for only the DAYOFWEEK portion of the expression
298 Returns:
299 The DAYOFWEEK description
301 """
303 if self._expression_parts[5] == "*":
304 # DOW is specified as * so we will not generate a description and defer to DOM part.
305 # Otherwise, we could get a contradiction like "on day 1 of the month, every day"
306 # or a dupe description like "every day, every day".
307 return ""
309 def get_day_name(s):
310 exp = s
311 if "#" in s:
312 exp, _ = s.split("#", 2)
313 elif "L" in s:
314 exp = exp.replace("L", '')
315 return ExpressionDescriptor.number_to_day(int(exp))
317 def get_format(s):
318 if "#" in s:
319 day_of_week_of_month = s[s.find("#") + 1:]
321 try:
322 day_of_week_of_month_number = int(day_of_week_of_month)
323 choices = {
324 1: self._("first"),
325 2: self._("second"),
326 3: self._("third"),
327 4: self._("forth"),
328 5: self._("fifth"),
329 }
330 day_of_week_of_month_description = choices.get(day_of_week_of_month_number, '')
331 except ValueError:
332 day_of_week_of_month_description = ''
334 formatted = "{}{}{}".format(self._(", on the "), day_of_week_of_month_description, self._(" {0} of the month"))
335 elif "L" in s:
336 formatted = self._(", on the last {0} of the month")
337 else:
338 formatted = self._(", only on {0}")
340 return formatted
342 return self.get_segment_description(
343 self._expression_parts[5],
344 self._(", every day"),
345 lambda s: get_day_name(s),
346 lambda s: self._(", every {0} days of the week").format(s),
347 lambda s: self._(", {0} through {1}"),
348 lambda s: get_format(s),
349 lambda s: self._(", {0} through {1}")
350 )
352 def get_month_description(self):
353 """Generates a description for only the MONTH portion of the expression
355 Returns:
356 The MONTH description
358 """
359 return self.get_segment_description(
360 self._expression_parts[4],
361 '',
362 lambda s: datetime.date(datetime.date.today().year, int(s), 1).strftime("%B"),
363 lambda s: self._(", every {0} months").format(s),
364 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}"),
365 lambda s: self._(", only in {0}"),
366 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}")
367 )
369 def get_day_of_month_description(self):
370 """Generates a description for only the DAYOFMONTH portion of the expression
372 Returns:
373 The DAYOFMONTH description
375 """
376 expression = self._expression_parts[3]
378 if expression == "L":
379 description = self._(", on the last day of the month")
380 elif expression == "LW" or expression == "WL":
381 description = self._(", on the last weekday of the month")
382 else:
383 regex = re.compile(r"(\d{1,2}W)|(W\d{1,2})")
384 m = regex.match(expression)
385 if m: # if matches
386 day_number = int(m.group().replace("W", ""))
388 day_string = self._("first weekday") if day_number == 1 else self._("weekday nearest day {0}").format(day_number)
389 description = self._(", on the {0} of the month").format(day_string)
390 else:
391 # Handle "last day offset"(i.e.L - 5: "5 days before the last day of the month")
392 regex = re.compile(r"L-(\d{1,2})")
393 m = regex.match(expression)
394 if m: # if matches
395 off_set_days = m.group(1)
396 description = self._(", {0} days before the last day of the month").format(off_set_days)
397 else:
398 description = self.get_segment_description(
399 expression,
400 self._(", every day"),
401 lambda s: s,
402 lambda s: self._(", every day") if s == "1" else self._(", every {0} days"),
403 lambda s: self._(", between day {0} and {1} of the month"),
404 lambda s: self._(", on day {0} of the month"),
405 lambda s: self._(", {0} through {1}")
406 )
408 return description
410 def get_year_description(self):
411 """Generates a description for only the YEAR portion of the expression
413 Returns:
414 The YEAR description
416 """
418 def format_year(s):
419 regex = re.compile(r"^\d+$")
420 if regex.match(s):
421 year_int = int(s)
422 if year_int < 1900:
423 return year_int
424 return datetime.date(year_int, 1, 1).strftime("%Y")
425 else:
426 return s
428 return self.get_segment_description(
429 self._expression_parts[6],
430 '',
431 lambda s: format_year(s),
432 lambda s: self._(", every {0} years").format(s),
433 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}"),
434 lambda s: self._(", only in {0}"),
435 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}")
436 )
438 def get_segment_description(
439 self,
440 expression,
441 all_description,
442 get_single_item_description,
443 get_interval_description_format,
444 get_between_description_format,
445 get_description_format,
446 get_range_format
447 ):
448 """Returns segment description
449 Args:
450 expression: Segment to descript
451 all_description: *
452 get_single_item_description: 1
453 get_interval_description_format: 1/2
454 get_between_description_format: 1-2
455 get_description_format: format get_single_item_description
456 get_range_format: function that formats range expressions depending on cron parts
457 Returns:
458 segment description
460 """
461 description = None
462 if expression is None or expression == '':
463 description = ''
464 elif expression == "*":
465 description = all_description
466 elif any(ext in expression for ext in ['/', '-', ',']) is False:
467 description = get_description_format(expression).format(get_single_item_description(expression))
468 elif "/" in expression:
469 segments = expression.split('/')
470 description = get_interval_description_format(segments[1]).format(get_single_item_description(segments[1]))
472 # interval contains 'between' piece (i.e. 2-59/3 )
473 if "-" in segments[0]:
474 between_segment_description = self.generate_between_segment_description(
475 segments[0],
476 get_between_description_format,
477 get_single_item_description
478 )
479 if not between_segment_description.startswith(", "):
480 description += ", "
482 description += between_segment_description
483 elif any(ext in segments[0] for ext in ['*', ',']) is False:
484 range_item_description = get_description_format(segments[0]).format(
485 get_single_item_description(segments[0])
486 )
487 range_item_description = range_item_description.replace(", ", "")
489 description += self._(", starting {0}").format(range_item_description)
490 elif "," in expression:
491 segments = expression.split(',')
493 description_content = ''
494 for i, segment in enumerate(segments):
495 if i > 0 and len(segments) > 2:
496 description_content += ","
498 if i < len(segments) - 1:
499 description_content += " "
501 if i > 0 and len(segments) > 1 and (i == len(segments) - 1 or len(segments) == 2):
502 description_content += self._(" and ")
504 if "-" in segment:
505 between_segment_description = self.generate_between_segment_description(
506 segment,
507 get_range_format,
508 get_single_item_description
509 )
511 between_segment_description = between_segment_description.replace(", ", "")
513 description_content += between_segment_description
514 else:
515 description_content += get_single_item_description(segment)
517 description = get_description_format(expression).format(description_content)
518 elif "-" in expression:
519 description = self.generate_between_segment_description(
520 expression,
521 get_between_description_format,
522 get_single_item_description
523 )
525 return description
527 def generate_between_segment_description(
528 self,
529 between_expression,
530 get_between_description_format,
531 get_single_item_description
532 ):
533 """
534 Generates the between segment description
535 :param between_expression:
536 :param get_between_description_format:
537 :param get_single_item_description:
538 :return: The between segment description
539 """
540 description = ""
541 between_segments = between_expression.split('-')
542 between_segment_1_description = get_single_item_description(between_segments[0])
543 between_segment_2_description = get_single_item_description(between_segments[1])
544 between_segment_2_description = between_segment_2_description.replace(":00", ":59")
546 between_description_format = get_between_description_format(between_expression)
547 description += between_description_format.format(between_segment_1_description, between_segment_2_description)
549 return description
551 def format_time(
552 self,
553 hour_expression,
554 minute_expression,
555 second_expression=''
556 ):
557 """Given time parts, will construct a formatted time description
558 Args:
559 hour_expression: Hours part
560 minute_expression: Minutes part
561 second_expression: Seconds part
562 Returns:
563 Formatted time description
565 """
566 hour = int(hour_expression)
568 period = ''
569 if self._options.use_24hour_time_format is False:
570 period = self._("PM") if (hour >= 12) else self._("AM")
571 if period:
572 # add preceding space
573 period = " " + period
575 if hour > 12:
576 hour -= 12
578 if hour == 0:
579 hour = 12
581 minute = str(int(minute_expression)) # Removes leading zero if any
582 second = ''
583 if second_expression is not None and second_expression:
584 second = "{}{}".format(":", str(int(second_expression)).zfill(2))
586 return "{0}:{1}{2}{3}".format(str(hour).zfill(2), minute.zfill(2), second, period)
588 def transform_verbosity(self, description, use_verbose_format):
589 """Transforms the verbosity of the expression description by stripping verbosity from original description
590 Args:
591 description: The description to transform
592 use_verbose_format: If True, will leave description as it, if False, will strip verbose parts
593 Returns:
594 The transformed description with proper verbosity
596 """
597 if use_verbose_format is False:
598 description = description.replace(self._(", every minute"), '')
599 description = description.replace(self._(", every hour"), '')
600 description = description.replace(self._(", every day"), '')
601 description = re.sub(r', ?$', '', description)
602 return description
604 @staticmethod
605 def transform_case(description, case_type):
606 """Transforms the case of the expression description, based on options
607 Args:
608 description: The description to transform
609 case_type: The casing type that controls the output casing
610 Returns:
611 The transformed description with proper casing
612 """
613 if case_type == CasingTypeEnum.Sentence:
614 description = "{}{}".format(
615 description[0].upper(),
616 description[1:])
617 elif case_type == CasingTypeEnum.Title:
618 description = description.title()
619 else:
620 description = description.lower()
621 return description
623 @staticmethod
624 def number_to_day(day_number):
625 """Returns localized day name by its CRON number
627 Args:
628 day_number: Number of a day
629 Returns:
630 Day corresponding to day_number
631 Raises:
632 IndexError: When day_number is not found
633 """
634 try:
635 return [
636 calendar.day_name[6],
637 calendar.day_name[0],
638 calendar.day_name[1],
639 calendar.day_name[2],
640 calendar.day_name[3],
641 calendar.day_name[4],
642 calendar.day_name[5]
643 ][day_number]
644 except IndexError:
645 raise IndexError("Day {} is out of range!".format(day_number))
647 def __str__(self):
648 return self.get_description()
650 def __repr__(self):
651 return self.get_description()
654def get_description(expression, options=None):
655 """Generates a human readable string for the Cron Expression
656 Args:
657 expression: The cron expression string
658 options: Options to control the output description
659 Returns:
660 The cron expression description
662 """
663 descripter = ExpressionDescriptor(expression, options)
664 return descripter.get_description(DescriptionTypeEnum.FULL)