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 dateutil import rrule 

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

8from django.utils.timezone import now 

9from django.conf import settings 

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

11 localize_time_range 

12from jutil.email import send_email 

13import getpass 

14from django.utils import translation 

15from jutil.parse import parse_datetime 

16 

17 

18logger = logging.getLogger(__name__) 

19 

20 

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

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

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

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