Coverage for jutil/command.py : 79%

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
17logger = logging.getLogger(__name__)
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
38 def do(self, *args, **kwargs):
39 pass
42def add_date_range_arguments(parser: CommandParser):
43 """
44 Adds following arguments to the CommandParser:
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 --prev-2d
73 --plus-minus-2d
74 --next-2d
75 --prev-1d
76 --plus-minus-1d
77 --next-1d
79 Steps:
80 --daily
81 --weekly
82 --monthly
84 :param parser:
85 :return:
86 """
87 parser.add_argument('--begin', type=str)
88 parser.add_argument('--end', type=str)
89 for v in TIME_STEP_NAMES:
90 parser.add_argument('--' + v.replace('_', '-'), action='store_true')
91 for v in TIME_RANGE_NAMES:
92 parser.add_argument('--' + v.replace('_', '-'), action='store_true')
95def get_date_range_by_name(name: str, today: Optional[datetime] = None, tz: Any = None) -> Tuple[datetime, datetime]:
96 """
97 Returns a timezone-aware date range by symbolic name.
98 :param name: Name of the date range. See add_date_range_arguments().
99 :param today: Optional current datetime. Default is now().
100 :param tz: Optional timezone. Default is UTC.
101 :return: datetime (begin, end)
102 """
103 if today is None:
104 today = datetime.utcnow()
105 if name == 'last_week':
106 return last_week(today, tz)
107 if name == 'last_month':
108 return last_month(today, tz)
109 if name == 'last_year':
110 return last_year(today, tz)
111 if name == 'this_week':
112 return this_week(today, tz)
113 if name == 'this_month':
114 return this_month(today, tz)
115 if name == 'this_year':
116 return this_year(today, tz)
117 if name == 'yesterday':
118 return yesterday(today, tz)
119 if name == 'today':
120 begin = today.replace(hour=0, minute=0, second=0, microsecond=0)
121 end = begin + timedelta(hours=24)
122 return localize_time_range(begin, end, tz)
123 m = re.match(r'^plus_minus_(\d+)d$', name)
124 if m:
125 days = int(m.group(1))
126 return localize_time_range(today - timedelta(days=days), today + timedelta(days=days), tz)
127 m = re.match(r'^prev_(\d+)d$', name)
128 if m:
129 days = int(m.group(1))
130 return localize_time_range(today - timedelta(days=days), today, tz)
131 m = re.match(r'^next_(\d+)d$', name)
132 if m: 132 ↛ 135line 132 didn't jump to line 135, because the condition on line 132 was never false
133 days = int(m.group(1))
134 return localize_time_range(today, today + timedelta(days=days), tz)
135 raise ValueError('Invalid date range name: {}'.format(name))
138def parse_date_range_arguments(options: dict, default_range: str = 'last_month') -> Tuple[datetime, datetime, List[Tuple[datetime, datetime]]]:
139 """
140 Parses date range from input and returns timezone-aware date range and
141 interval list according to 'step' name argument (optional).
142 See add_date_range_arguments()
143 :param options: Parsed arguments passed to the command
144 :param default_range: Default datetime range to return if no other selected
145 :return: begin, end, [(begin1,end1), (begin2,end2), ...]
146 """
147 begin, end = get_date_range_by_name(default_range)
148 for range_name in TIME_RANGE_NAMES:
149 if options.get(range_name): 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true
150 begin, end = get_date_range_by_name(range_name)
151 if options.get('begin'): 151 ↛ 154line 151 didn't jump to line 154, because the condition on line 151 was never false
152 begin = parse_datetime(options['begin']) # type: ignore
153 end = now()
154 if options.get('end'): 154 ↛ 157line 154 didn't jump to line 157, because the condition on line 154 was never false
155 end = parse_datetime(options['end']) # type: ignore
157 step_type = ''
158 for step_name in TIME_STEP_NAMES:
159 if options.get(step_name): 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true
160 if step_type:
161 raise ValueError('Cannot use --{} and --{} simultaneously'.format(step_type, step_name))
162 step_type = step_name
163 if step_type: 163 ↛ 164line 163 didn't jump to line 164, because the condition on line 163 was never true
164 steps = get_time_steps(step_type, begin, end)
165 else:
166 steps = [(begin, end)]
167 return begin, end, steps