Coverage for jutil/command.py : 72%

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
18logger = logging.getLogger(__name__)
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
39 def do(self, *args, **kwargs):
40 pass
43def add_date_range_arguments(parser: CommandParser):
44 """
45 Adds following arguments to the CommandParser:
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
72 Steps:
73 --daily
74 --weekly
75 --monthly
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')
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))
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
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