Coverage for jutil/command.py: 72%

# Coverage for jutil/command.py : 72%

## 94 statements

Hot-keys on this page

r m x p toggle line displays

j k next/prev highlighted chunk

0 (zero) top of page

1 (one) first highlighted chunk

1 import logging

2 import re

3 import traceback

4 from datetime import datetime, timedelta

5 from typing import Tuple, List, Any, Optional

6 from dateutil import rrule

7 from django.core.management.base import BaseCommand, CommandParser

8 from django.utils.timezone import now

9 from django.conf import settings

10 from jutil.dates import last_month, yesterday, TIME_RANGE_NAMES, TIME_STEP_NAMES, this_month, last_year, last_week, \

11 localize_time_range

12 from jutil.email import send_email

13 import getpass

14 from django.utils import translation

15 from jutil.parse import parse_datetime

16

17

18 logger = logging.getLogger(__name__)

19

20

21 class SafeCommand(BaseCommand):

22 """

23 BaseCommand which catches, logs and emails errors.

24 Uses list of emails from settings.ADMINS.

25 Implement do() in derived classes.

26 """

27 def handle(self, *args, **options):

28 try:

29 if hasattr(settings, 'LANGUAGE_CODE'):

30 translation.activate(settings.LANGUAGE_CODE)

31 return self.do(*args, **options)

32 except Exception as e:

33 msg = "ERROR: {} {}".format(str(e), traceback.format_exc())

34 logger.error(msg)

35 if not settings.DEBUG:

36 send_email(settings.ADMINS, 'Error @ {}'.format(getpass.getuser()), msg)

37 raise

38

39 def do(self, *args, **kwargs):

40 pass

41

42

43 def add_date_range_arguments(parser: CommandParser):

44 """

45 Adds following arguments to the CommandParser:

46

47 Ranges:

48 --begin BEGIN

49 --end END

50 --last-month

51 --last-year

52 --this-month

53 --last-week

54 --yesterday

55 --today

56 --prev-90d

57 --plus-minus-90d

58 --next-90d

59 --prev-60d

60 --plus-minus-60d

61 --next-60d

62 --prev-30d

63 --plus-minus-30d

64 --next-30d

65 --prev-15d

66 --plus-minus-15d

67 --next-15d

68 --prev-7d

69 --plus-minus-7d

70 --next-7d

71

72 Steps:

73 --daily

74 --weekly

75 --monthly

76

77 :param parser:

78 :return:

79 """

80 parser.add_argument('--begin', type=str)

81 parser.add_argument('--end', type=str)

82 for v in TIME_STEP_NAMES:

83 parser.add_argument('--' + v.replace('_', '-'), action='store_true')

84 for v in TIME_RANGE_NAMES:

85 parser.add_argument('--' + v.replace('_', '-'), action='store_true')

86

87

88 def get_date_range_by_name(name: str, today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:

89 """

90 Returns a timezone-aware date range by symbolic name.

91 :param name: Name of the date range. See add_date_range_arguments().

92 :param today: Optional current datetime. Default is now().

93 :param tz: Optional timezone. Default is UTC.

94 :return: datetime (begin, end)

95 """

96 if today is None:

97 today = datetime.utcnow()

98 if name == 'last_month':

99 return last_month(today, tz)

100 if name == 'last_week':

101 return last_week(today, tz)

102 if name == 'this_month':

103 return this_month(today, tz)

104 if name == 'last_year':

105 return last_year(today, tz)

106 if name == 'yesterday':

107 return yesterday(today, tz)

108 if name == 'today':

109 begin = today.replace(hour=0, minute=0, second=0, microsecond=0)

110 end = begin + timedelta(hours=24)

111 return localize_time_range(begin, end, tz)

112 m = re.match(r'^plus_minus_(\d+)d$', name)

113 if m:

114 days = int(m.group(1))

115 return localize_time_range(today - timedelta(days=days), today + timedelta(days=days), tz)

116 m = re.match(r'^prev_(\d+)d$', name)

117 if m:

118 days = int(m.group(1))

119 return localize_time_range(today - timedelta(days=days), today, tz)

120 m = re.match(r'^next_(\d+)d$', name)

121 if m: 121 ↛ 124line 121 didn't jump to line 124, because the condition on line 121 was never false

122 days = int(m.group(1))

123 return localize_time_range(today, today + timedelta(days=days), tz)

124 raise ValueError('Invalid date range name: {}'.format(name))

125

126

127 def parse_date_range_arguments(options: dict, default_range: str = 'last_month') -> Tuple[datetime, datetime, List[Tuple[datetime, datetime]]]:

128 """

129 Parses date range from input and returns timezone-aware date range and

130 interval list according to 'step' name argument (optional).

131 See add_date_range_arguments()

132 :param options: Parsed arguments passed to the command

133 :param default_range: Default datetime range to return if no other selected

134 :return: begin, end, [(begin1,end1), (begin2,end2), ...]

135 """

136 begin, end = get_date_range_by_name(default_range)

137 for range_name in TIME_RANGE_NAMES:

138 if options.get(range_name): 138 ↛ 139line 138 didn't jump to line 139, because the condition on line 138 was never true

139 begin, end = get_date_range_by_name(range_name)

140 if options.get('begin'): 140 ↛ 143line 140 didn't jump to line 143, because the condition on line 140 was never false

141 begin = parse_datetime(options['begin']) # type: ignore

142 end = now()

143 if options.get('end'): 143 ↛ 146line 143 didn't jump to line 146, because the condition on line 143 was never false

144 end = parse_datetime(options['end']) # type: ignore

145

146 step_type = None

147 after_end = end

148 for step_name in TIME_STEP_NAMES:

149 if options.get(step_name): 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true

150 step_type = getattr(rrule, step_name.upper())

151 if rrule.DAILY == step_type:

152 after_end += timedelta(days=1)

153 if rrule.WEEKLY == step_type:

154 after_end += timedelta(days=7)

155 if rrule.MONTHLY == step_type:

156 after_end += timedelta(days=31)

157 steps = None

158 if step_type: 158 ↛ 159line 158 didn't jump to line 159, because the condition on line 158 was never true

159 begins = list(rrule.rrule(step_type, dtstart=begin, until=after_end))

160 steps = [(begins[i], begins[i+1]) for i in range(len(begins)-1)]

161 if steps is None: 161 ↛ 163line 161 didn't jump to line 163, because the condition on line 161 was never false

162 steps = [(begin, end)]

163 return begin, end, steps