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 ( 

10 last_month, 

11 yesterday, 

12 TIME_RANGE_NAMES, 

13 TIME_STEP_NAMES, 

14 this_month, 

15 last_year, 

16 last_week, 

17 localize_time_range, 

18 this_year, 

19 this_week, 

20 get_time_steps, 

21) 

22from jutil.email import send_email 

23import getpass 

24from django.utils import translation 

25from jutil.parse import parse_datetime 

26 

27 

28logger = logging.getLogger(__name__) 

29 

30 

31class SafeCommand(BaseCommand): 

32 """ 

33 BaseCommand which catches, logs and emails errors. 

34 Uses list of emails from settings.ADMINS. 

35 Implement do() in derived classes. 

36 """ 

37 

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

39 try: 

40 if hasattr(settings, "LANGUAGE_CODE"): 

41 translation.activate(settings.LANGUAGE_CODE) 

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

43 except Exception as e: 

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

45 logger.error(msg) 

46 if not settings.DEBUG: 

47 send_email(settings.ADMINS, "Error @ {}".format(getpass.getuser()), msg) 

48 raise 

49 

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

51 pass 

52 

53 

54def add_date_range_arguments(parser: CommandParser): 

55 """ 

56 Adds following arguments to the CommandParser: 

57 

58 Ranges: 

59 --begin BEGIN 

60 --end END 

61 --last-year 

62 --last-month 

63 --last-week 

64 --this-year 

65 --this-month 

66 --this-week 

67 --yesterday 

68 --today 

69 --prev-90d 

70 --plus-minus-90d 

71 --next-90d 

72 --prev-60d 

73 --plus-minus-60d 

74 --next-60d 

75 --prev-30d 

76 --plus-minus-30d 

77 --next-30d 

78 --prev-15d 

79 --plus-minus-15d 

80 --next-15d 

81 --prev-7d 

82 --plus-minus-7d 

83 --next-7d 

84 --prev-2d 

85 --plus-minus-2d 

86 --next-2d 

87 --prev-1d 

88 --plus-minus-1d 

89 --next-1d 

90 

91 Steps: 

92 --daily 

93 --weekly 

94 --monthly 

95 

96 :param parser: 

97 :return: 

98 """ 

99 parser.add_argument("--begin", type=str) 

100 parser.add_argument("--end", type=str) 

101 for v in TIME_STEP_NAMES: 

102 parser.add_argument("--" + v.replace("_", "-"), action="store_true") 

103 for v in TIME_RANGE_NAMES: 

104 parser.add_argument("--" + v.replace("_", "-"), action="store_true") 

105 

106 

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

108 """ 

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

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

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

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

113 :return: datetime (begin, end) 

114 """ 

115 if today is None: 

116 today = datetime.utcnow() 

117 if name == "last_week": 

118 return last_week(today, tz) 

119 if name == "last_month": 

120 return last_month(today, tz) 

121 if name == "last_year": 

122 return last_year(today, tz) 

123 if name == "this_week": 

124 return this_week(today, tz) 

125 if name == "this_month": 

126 return this_month(today, tz) 

127 if name == "this_year": 

128 return this_year(today, tz) 

129 if name == "yesterday": 

130 return yesterday(today, tz) 

131 if name == "today": 

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

133 end = begin + timedelta(hours=24) 

134 return localize_time_range(begin, end, tz) 

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

136 if m: 

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

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

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

140 if m: 

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

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

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

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

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

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

147 raise ValueError("Invalid date range name: {}".format(name)) 

148 

149 

150def parse_date_range_arguments( 

151 options: dict, default_range: str = "last_month" 

152) -> Tuple[datetime, datetime, List[Tuple[datetime, datetime]]]: 

153 """ 

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

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

156 See add_date_range_arguments() 

157 :param options: Parsed arguments passed to the command 

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

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

160 """ 

161 begin, end = get_date_range_by_name(default_range) 

162 for range_name in TIME_RANGE_NAMES: 

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

164 begin, end = get_date_range_by_name(range_name) 

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

166 begin = parse_datetime(options["begin"]) # type: ignore 

167 end = now() 

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

169 end = parse_datetime(options["end"]) # type: ignore 

170 

171 step_type = "" 

172 for step_name in TIME_STEP_NAMES: 

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

174 if step_type: 

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

176 step_type = step_name 

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

178 steps = get_time_steps(step_type, begin, end) 

179 else: 

180 steps = [(begin, end)] 

181 return begin, end, steps