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 (
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
28logger = logging.getLogger(__name__)
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 """
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
50 def do(self, *args, **kwargs):
51 pass
54def add_date_range_arguments(parser: CommandParser):
55 """
56 Adds following arguments to the CommandParser:
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
91 Steps:
92 --daily
93 --weekly
94 --monthly
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")
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))
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
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