Coverage for jutil/command.py: 79%

# Coverage for jutil/command.py : 79%

## 89 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 django.core.management.base import BaseCommand, CommandParser

7 from django.utils.timezone import now

8 from django.conf import settings

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

10 localize_time_range, this_year, this_week, get_time_steps

11 from jutil.email import send_email

12 import getpass

13 from django.utils import translation

14 from jutil.parse import parse_datetime

15

16

17 logger = logging.getLogger(__name__)

18

19

20 class SafeCommand(BaseCommand):

21 """

22 BaseCommand which catches, logs and emails errors.

23 Uses list of emails from settings.ADMINS.

24 Implement do() in derived classes.

25 """

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

27 try:

28 if hasattr(settings, 'LANGUAGE_CODE'):

29 translation.activate(settings.LANGUAGE_CODE)

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

31 except Exception as e:

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

33 logger.error(msg)

34 if not settings.DEBUG:

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

36 raise

37

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

39 pass

40

41

42 def add_date_range_arguments(parser: CommandParser):

43 """

44 Adds following arguments to the CommandParser:

45

46 Ranges:

47 --begin BEGIN

48 --end END

49 --last-year

50 --last-month

51 --last-week

52 --this-year

53 --this-month

54 --this-week

55 --yesterday

56 --today

57 --prev-90d

58 --plus-minus-90d

59 --next-90d

60 --prev-60d

61 --plus-minus-60d

62 --next-60d

63 --prev-30d

64 --plus-minus-30d

65 --next-30d

66 --prev-15d

67 --plus-minus-15d

68 --next-15d

69 --prev-7d

70 --plus-minus-7d

71 --next-7d

72

73 Steps:

74 --daily

75 --weekly

76 --monthly

77

78 :param parser:

79 :return:

80 """

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

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

83 for v in TIME_STEP_NAMES:

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

85 for v in TIME_RANGE_NAMES:

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

87

88

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

90 """

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

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

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

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

95 :return: datetime (begin, end)

96 """

97 if today is None:

98 today = datetime.utcnow()

99 if name == 'last_week':

100 return last_week(today, tz)

101 if name == 'last_month':

102 return last_month(today, tz)

103 if name == 'last_year':

104 return last_year(today, tz)

105 if name == 'this_week':

106 return this_week(today, tz)

107 if name == 'this_month':

108 return this_month(today, tz)

109 if name == 'this_year':

110 return this_year(today, tz)

111 if name == 'yesterday':

112 return yesterday(today, tz)

113 if name == 'today':

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

115 end = begin + timedelta(hours=24)

116 return localize_time_range(begin, end, tz)

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

118 if m:

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

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

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

122 if m:

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

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

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

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

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

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

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

130

131

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

133 """

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

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

136 See add_date_range_arguments()

137 :param options: Parsed arguments passed to the command

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

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

140 """

141 begin, end = get_date_range_by_name(default_range)

142 for range_name in TIME_RANGE_NAMES:

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

144 begin, end = get_date_range_by_name(range_name)

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

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

147 end = now()

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

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

150

151 step_type = ''

152 for step_name in TIME_STEP_NAMES:

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

154 if step_type:

155 raise ValueError('Cannot use --{} and --{} simultaneously'.format(step_type, step_name))

156 step_type = step_name

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

158 steps = get_time_steps(step_type, begin, end)

159 else:

160 steps = [(begin, end)]

161 return begin, end, steps