Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/cron_descriptor/ExpressionDescriptor.py: 43%
253 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:35 +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 = []
48 def __init__(self, expression, options=None, **kwargs):
49 """Initializes a new instance of the ExpressionDescriptor
51 Args:
52 expression: The cron expression string
53 options: Options to control the output description
54 Raises:
55 WrongArgumentException: if kwarg is unknown
57 """
58 if options is None:
59 options = Options()
60 self._expression = expression
61 self._options = options
62 self._expression_parts = []
64 # if kwargs in _options, overwrite it, if not raise exception
65 for kwarg in kwargs:
66 if hasattr(self._options, kwarg):
67 setattr(self._options, kwarg, kwargs[kwarg])
68 else:
69 raise WrongArgumentException("Unknown {} configuration argument".format(kwarg))
71 # Initializes localization
72 self.get_text = GetText(options.locale_code, options.locale_location)
74 # Parse expression
75 parser = ExpressionParser(self._expression, self._options)
76 self._expression_parts = parser.parse()
78 def _(self, message):
79 return self.get_text.trans.gettext(message)
81 def get_description(self, description_type=DescriptionTypeEnum.FULL):
82 """Generates a humanreadable string for the Cron Expression
84 Args:
85 description_type: Which part(s) of the expression to describe
86 Returns:
87 The cron expression description
88 Raises:
89 Exception:
91 """
92 choices = {
93 DescriptionTypeEnum.FULL: self.get_full_description,
94 DescriptionTypeEnum.TIMEOFDAY: self.get_time_of_day_description,
95 DescriptionTypeEnum.HOURS: self.get_hours_description,
96 DescriptionTypeEnum.MINUTES: self.get_minutes_description,
97 DescriptionTypeEnum.SECONDS: self.get_seconds_description,
98 DescriptionTypeEnum.DAYOFMONTH: self.get_day_of_month_description,
99 DescriptionTypeEnum.MONTH: self.get_month_description,
100 DescriptionTypeEnum.DAYOFWEEK: self.get_day_of_week_description,
101 DescriptionTypeEnum.YEAR: self.get_year_description,
102 }
104 return choices.get(description_type, self.get_seconds_description)()
106 def get_full_description(self):
107 """Generates the FULL description
109 Returns:
110 The FULL description
111 Raises:
112 FormatException: if formatting fails
114 """
116 try:
117 time_segment = self.get_time_of_day_description()
118 day_of_month_desc = self.get_day_of_month_description()
119 month_desc = self.get_month_description()
120 day_of_week_desc = self.get_day_of_week_description()
121 year_desc = self.get_year_description()
123 description = "{0}{1}{2}{3}{4}".format(
124 time_segment,
125 day_of_month_desc,
126 day_of_week_desc,
127 month_desc,
128 year_desc)
130 description = self.transform_verbosity(description, self._options.verbose)
131 description = ExpressionDescriptor.transform_case(description, self._options.casing_type)
132 except Exception:
133 description = self._(
134 "An error occurred when generating the expression description. Check the cron expression syntax."
135 )
136 raise FormatException(description)
138 return description
140 def get_time_of_day_description(self):
141 """Generates a description for only the TIMEOFDAY portion of the expression
143 Returns:
144 The TIMEOFDAY description
146 """
147 seconds_expression = self._expression_parts[0]
148 minute_expression = self._expression_parts[1]
149 hour_expression = self._expression_parts[2]
151 description = StringBuilder()
153 # handle special cases first
154 if any(exp in minute_expression for exp in self._special_characters) is False and \
155 any(exp in hour_expression for exp in self._special_characters) is False and \
156 any(exp in seconds_expression for exp in self._special_characters) is False:
157 # specific time of day (i.e. 10 14)
158 description.append(self._("At "))
159 description.append(
160 self.format_time(
161 hour_expression,
162 minute_expression,
163 seconds_expression))
164 elif seconds_expression == "" and "-" in minute_expression and \
165 "," not in minute_expression and \
166 any(exp in hour_expression for exp in self._special_characters) is False:
167 # minute range in single hour (i.e. 0-10 11)
168 minute_parts = minute_expression.split('-')
169 description.append(self._("Every minute between {0} and {1}").format(
170 self.format_time(hour_expression, minute_parts[0]), self.format_time(hour_expression, minute_parts[1])))
171 elif seconds_expression == "" and "," in hour_expression and "-" not in hour_expression and \
172 any(exp in minute_expression for exp in self._special_characters) is False:
173 # hours list with single minute (o.e. 30 6,14,16)
174 hour_parts = hour_expression.split(',')
175 description.append(self._("At"))
176 for i, hour_part in enumerate(hour_parts):
177 description.append(" ")
178 description.append(self.format_time(hour_part, minute_expression))
180 if i < (len(hour_parts) - 2):
181 description.append(",")
183 if i == len(hour_parts) - 2:
184 description.append(self._(" and"))
185 else:
186 # default time description
187 seconds_description = self.get_seconds_description()
188 minutes_description = self.get_minutes_description()
189 hours_description = self.get_hours_description()
191 description.append(seconds_description)
193 if description and minutes_description:
194 description.append(", ")
196 description.append(minutes_description)
198 if description and hours_description:
199 description.append(", ")
201 description.append(hours_description)
202 return str(description)
204 def get_seconds_description(self):
205 """Generates a description for only the SECONDS portion of the expression
207 Returns:
208 The SECONDS description
210 """
212 def get_description_format(s):
213 if s == "0":
214 return ""
216 try:
217 if int(s) < 20:
218 return self._("at {0} seconds past the minute")
219 else:
220 return self._("at {0} seconds past the minute [grThen20]") or self._("at {0} seconds past the minute")
221 except ValueError:
222 return self._("at {0} seconds past the minute")
224 return self.get_segment_description(
225 self._expression_parts[0],
226 self._("every second"),
227 lambda s: s,
228 lambda s: self._("every {0} seconds").format(s),
229 lambda s: self._("seconds {0} through {1} past the minute"),
230 get_description_format,
231 lambda s: self._(", second {0} through second {1}") or self._(", {0} through {1}")
232 )
234 def get_minutes_description(self):
235 """Generates a description for only the MINUTE portion of the expression
237 Returns:
238 The MINUTE description
240 """
241 seconds_expression = self._expression_parts[0]
243 def get_description_format(s):
244 if s == "0" and seconds_expression == "":
245 return ""
247 try:
248 if int(s) < 20:
249 return self._("at {0} minutes past the hour")
250 else:
251 return self._("at {0} minutes past the hour [grThen20]") or self._("at {0} minutes past the hour")
252 except ValueError:
253 return self._("at {0} minutes past the hour")
255 return self.get_segment_description(
256 self._expression_parts[1],
257 self._("every minute"),
258 lambda s: s,
259 lambda s: self._("every {0} minutes").format(s),
260 lambda s: self._("minutes {0} through {1} past the hour"),
261 get_description_format,
262 lambda s: self._(", minute {0} through minute {1}") or self._(", {0} through {1}")
263 )
265 def get_hours_description(self):
266 """Generates a description for only the HOUR portion of the expression
268 Returns:
269 The HOUR description
271 """
272 expression = self._expression_parts[2]
273 return self.get_segment_description(
274 expression,
275 self._("every hour"),
276 lambda s: self.format_time(s, "0"),
277 lambda s: self._("every {0} hours").format(s),
278 lambda s: self._("between {0} and {1}"),
279 lambda s: self._("at {0}"),
280 lambda s: self._(", hour {0} through hour {1}") or self._(", {0} through {1}")
281 )
283 def get_day_of_week_description(self):
284 """Generates a description for only the DAYOFWEEK portion of the expression
286 Returns:
287 The DAYOFWEEK description
289 """
291 if self._expression_parts[5] == "*":
292 # DOW is specified as * so we will not generate a description and defer to DOM part.
293 # Otherwise, we could get a contradiction like "on day 1 of the month, every day"
294 # or a dupe description like "every day, every day".
295 return ""
297 def get_day_name(s):
298 exp = s
299 if "#" in s:
300 exp, _ = s.split("#", 2)
301 elif "L" in s:
302 exp = exp.replace("L", '')
303 return ExpressionDescriptor.number_to_day(int(exp))
305 def get_format(s):
306 if "#" in s:
307 day_of_week_of_month = s[s.find("#") + 1:]
309 try:
310 day_of_week_of_month_number = int(day_of_week_of_month)
311 choices = {
312 1: self._("first"),
313 2: self._("second"),
314 3: self._("third"),
315 4: self._("forth"),
316 5: self._("fifth"),
317 }
318 day_of_week_of_month_description = choices.get(day_of_week_of_month_number, '')
319 except ValueError:
320 day_of_week_of_month_description = ''
322 formatted = "{}{}{}".format(self._(", on the "), day_of_week_of_month_description, self._(" {0} of the month"))
323 elif "L" in s:
324 formatted = self._(", on the last {0} of the month")
325 else:
326 formatted = self._(", only on {0}")
328 return formatted
330 return self.get_segment_description(
331 self._expression_parts[5],
332 self._(", every day"),
333 lambda s: get_day_name(s),
334 lambda s: self._(", every {0} days of the week").format(s),
335 lambda s: self._(", {0} through {1}"),
336 lambda s: get_format(s),
337 lambda s: self._(", {0} through {1}")
338 )
340 def get_month_description(self):
341 """Generates a description for only the MONTH portion of the expression
343 Returns:
344 The MONTH description
346 """
347 return self.get_segment_description(
348 self._expression_parts[4],
349 '',
350 lambda s: datetime.date(datetime.date.today().year, int(s), 1).strftime("%B"),
351 lambda s: self._(", every {0} months").format(s),
352 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}"),
353 lambda s: self._(", only in {0}"),
354 lambda s: self._(", month {0} through month {1}") or self._(", {0} through {1}")
355 )
357 def get_day_of_month_description(self):
358 """Generates a description for only the DAYOFMONTH portion of the expression
360 Returns:
361 The DAYOFMONTH description
363 """
364 expression = self._expression_parts[3]
366 if expression == "L":
367 description = self._(", on the last day of the month")
368 elif expression == "LW" or expression == "WL":
369 description = self._(", on the last weekday of the month")
370 else:
371 regex = re.compile(r"(\d{1,2}W)|(W\d{1,2})")
372 m = regex.match(expression)
373 if m: # if matches
374 day_number = int(m.group().replace("W", ""))
376 day_string = self._("first weekday") if day_number == 1 else self._("weekday nearest day {0}").format(day_number)
377 description = self._(", on the {0} of the month").format(day_string)
378 else:
379 # Handle "last day offset"(i.e.L - 5: "5 days before the last day of the month")
380 regex = re.compile(r"L-(\d{1,2})")
381 m = regex.match(expression)
382 if m: # if matches
383 off_set_days = m.group(1)
384 description = self._(", {0} days before the last day of the month").format(off_set_days)
385 else:
386 description = self.get_segment_description(
387 expression,
388 self._(", every day"),
389 lambda s: s,
390 lambda s: self._(", every day") if s == "1" else self._(", every {0} days"),
391 lambda s: self._(", between day {0} and {1} of the month"),
392 lambda s: self._(", on day {0} of the month"),
393 lambda s: self._(", {0} through {1}")
394 )
396 return description
398 def get_year_description(self):
399 """Generates a description for only the YEAR portion of the expression
401 Returns:
402 The YEAR description
404 """
406 def format_year(s):
407 regex = re.compile(r"^\d+$")
408 if regex.match(s):
409 year_int = int(s)
410 if year_int < 1900:
411 return year_int
412 return datetime.date(year_int, 1, 1).strftime("%Y")
413 else:
414 return s
416 return self.get_segment_description(
417 self._expression_parts[6],
418 '',
419 lambda s: format_year(s),
420 lambda s: self._(", every {0} years").format(s),
421 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}"),
422 lambda s: self._(", only in {0}"),
423 lambda s: self._(", year {0} through year {1}") or self._(", {0} through {1}")
424 )
426 def get_segment_description(
427 self,
428 expression,
429 all_description,
430 get_single_item_description,
431 get_interval_description_format,
432 get_between_description_format,
433 get_description_format,
434 get_range_format
435 ):
436 """Returns segment description
437 Args:
438 expression: Segment to descript
439 all_description: *
440 get_single_item_description: 1
441 get_interval_description_format: 1/2
442 get_between_description_format: 1-2
443 get_description_format: format get_single_item_description
444 get_range_format: function that formats range expressions depending on cron parts
445 Returns:
446 segment description
448 """
449 description = None
450 if expression is None or expression == '':
451 description = ''
452 elif expression == "*":
453 description = all_description
454 elif any(ext in expression for ext in ['/', '-', ',']) is False:
455 description = get_description_format(expression).format(get_single_item_description(expression))
456 elif "/" in expression:
457 segments = expression.split('/')
458 description = get_interval_description_format(segments[1]).format(get_single_item_description(segments[1]))
460 # interval contains 'between' piece (i.e. 2-59/3 )
461 if "-" in segments[0]:
462 between_segment_description = self.generate_between_segment_description(
463 segments[0],
464 get_between_description_format,
465 get_single_item_description
466 )
467 if not between_segment_description.startswith(", "):
468 description += ", "
470 description += between_segment_description
471 elif any(ext in segments[0] for ext in ['*', ',']) is False:
472 range_item_description = get_description_format(segments[0]).format(
473 get_single_item_description(segments[0])
474 )
475 range_item_description = range_item_description.replace(", ", "")
477 description += self._(", starting {0}").format(range_item_description)
478 elif "," in expression:
479 segments = expression.split(',')
481 description_content = ''
482 for i, segment in enumerate(segments):
483 if i > 0 and len(segments) > 2:
484 description_content += ","
486 if i < len(segments) - 1:
487 description_content += " "
489 if i > 0 and len(segments) > 1 and (i == len(segments) - 1 or len(segments) == 2):
490 description_content += self._(" and ")
492 if "-" in segment:
493 between_segment_description = self.generate_between_segment_description(
494 segment,
495 get_range_format,
496 get_single_item_description
497 )
499 between_segment_description = between_segment_description.replace(", ", "")
501 description_content += between_segment_description
502 else:
503 description_content += get_single_item_description(segment)
505 description = get_description_format(expression).format(description_content)
506 elif "-" in expression:
507 description = self.generate_between_segment_description(
508 expression,
509 get_between_description_format,
510 get_single_item_description
511 )
513 return description
515 def generate_between_segment_description(
516 self,
517 between_expression,
518 get_between_description_format,
519 get_single_item_description
520 ):
521 """
522 Generates the between segment description
523 :param between_expression:
524 :param get_between_description_format:
525 :param get_single_item_description:
526 :return: The between segment description
527 """
528 description = ""
529 between_segments = between_expression.split('-')
530 between_segment_1_description = get_single_item_description(between_segments[0])
531 between_segment_2_description = get_single_item_description(between_segments[1])
532 between_segment_2_description = between_segment_2_description.replace(":00", ":59")
534 between_description_format = get_between_description_format(between_expression)
535 description += between_description_format.format(between_segment_1_description, between_segment_2_description)
537 return description
539 def format_time(
540 self,
541 hour_expression,
542 minute_expression,
543 second_expression=''
544 ):
545 """Given time parts, will construct a formatted time description
546 Args:
547 hour_expression: Hours part
548 minute_expression: Minutes part
549 second_expression: Seconds part
550 Returns:
551 Formatted time description
553 """
554 hour = int(hour_expression)
556 period = ''
557 if self._options.use_24hour_time_format is False:
558 period = self._("PM") if (hour >= 12) else self._("AM")
559 if period:
560 # add preceding space
561 period = " " + period
563 if hour > 12:
564 hour -= 12
566 if hour == 0:
567 hour = 12
569 minute = str(int(minute_expression)) # Removes leading zero if any
570 second = ''
571 if second_expression is not None and second_expression:
572 second = "{}{}".format(":", str(int(second_expression)).zfill(2))
574 return "{0}:{1}{2}{3}".format(str(hour).zfill(2), minute.zfill(2), second, period)
576 def transform_verbosity(self, description, use_verbose_format):
577 """Transforms the verbosity of the expression description by stripping verbosity from original description
578 Args:
579 description: The description to transform
580 use_verbose_format: If True, will leave description as it, if False, will strip verbose parts
581 Returns:
582 The transformed description with proper verbosity
584 """
585 if use_verbose_format is False:
586 description = description.replace(self._(", every minute"), '')
587 description = description.replace(self._(", every hour"), '')
588 description = description.replace(self._(", every day"), '')
589 description = re.sub(r', ?$', '', description)
590 return description
592 @staticmethod
593 def transform_case(description, case_type):
594 """Transforms the case of the expression description, based on options
595 Args:
596 description: The description to transform
597 case_type: The casing type that controls the output casing
598 Returns:
599 The transformed description with proper casing
600 """
601 if case_type == CasingTypeEnum.Sentence:
602 description = "{}{}".format(
603 description[0].upper(),
604 description[1:])
605 elif case_type == CasingTypeEnum.Title:
606 description = description.title()
607 else:
608 description = description.lower()
609 return description
611 @staticmethod
612 def number_to_day(day_number):
613 """Returns localized day name by its CRON number
615 Args:
616 day_number: Number of a day
617 Returns:
618 Day corresponding to day_number
619 Raises:
620 IndexError: When day_number is not found
621 """
622 try:
623 return [
624 calendar.day_name[6],
625 calendar.day_name[0],
626 calendar.day_name[1],
627 calendar.day_name[2],
628 calendar.day_name[3],
629 calendar.day_name[4],
630 calendar.day_name[5]
631 ][day_number]
632 except IndexError:
633 raise IndexError("Day {} is out of range!".format(day_number))
635 def __str__(self):
636 return self.get_description()
638 def __repr__(self):
639 return self.get_description()
642def get_description(expression, options=None):
643 """Generates a human readable string for the Cron Expression
644 Args:
645 expression: The cron expression string
646 options: Options to control the output description
647 Returns:
648 The cron expression description
650 """
651 descripter = ExpressionDescriptor(expression, options)
652 return descripter.get_description(DescriptionTypeEnum.FULL)