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
73 Steps:
74 --daily
75 --weekly
76 --monthly
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')
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))
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
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