Hide keyboard shortcuts

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

1import logging 

2import re 

3import traceback 

4from datetime import datetime, timedelta 

5from typing import Tuple, List, Any, Optional 

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

7from django.utils.timezone import now 

8from django.conf import settings 

9from 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 

11from jutil.email import send_email 

12import getpass 

13from django.utils import translation 

14from jutil.parse import parse_datetime 

15 

16 

17logger = logging.getLogger(__name__) 

18 

19 

20class 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 

42def 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 

89def 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 

132def 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