Skip to content

Commit ccad94a

Browse files
authored
Code improvements to time processing routines (#1010)
1 parent a18e879 commit ccad94a

File tree

10 files changed

+179
-144
lines changed

10 files changed

+179
-144
lines changed

requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ scipy==1.6.2
1111
tenacity==7.0.0
1212
newrelic
1313
epiweeks==2.1.2
14+
typing-extensions

src/server/_params.py

+44-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

99
from ._exceptions import ValidationFailedException
10-
from .utils import days_in_range, weeks_in_range, guess_time_value_is_day
10+
from .utils import days_in_range, weeks_in_range, guess_time_value_is_day, guess_time_value_is_week, TimeValues, days_to_ranges, weeks_to_ranges
1111

1212

1313
def _parse_common_multi_arg(key: str) -> List[Tuple[str, Union[bool, Sequence[str]]]]:
@@ -108,7 +108,7 @@ def parse_single_source_signal_arg(key: str) -> SourceSignalPair:
108108
@dataclass
109109
class TimePair:
110110
time_type: str
111-
time_values: Union[bool, Sequence[Union[int, Tuple[int, int]]]]
111+
time_values: Union[bool, TimeValues]
112112

113113
@property
114114
def is_week(self) -> bool:
@@ -129,6 +129,16 @@ def count(self) -> float:
129129
return sum(1 if isinstance(v, int) else weeks_in_range(v) for v in self.time_values)
130130
return sum(1 if isinstance(v, int) else days_in_range(v) for v in self.time_values)
131131

132+
def to_ranges(self):
133+
"""
134+
returns this pair with times converted to ranges
135+
"""
136+
if isinstance(self.time_values, bool):
137+
return TimePair(self.time_type, self.time_values)
138+
if self.time_type == 'week':
139+
return TimePair(self.time_type, weeks_to_ranges(self.time_values))
140+
return TimePair(self.time_type, days_to_ranges(self.time_values))
141+
132142

133143
def _verify_range(start: int, end: int) -> Union[int, Tuple[int, int]]:
134144
if start == end:
@@ -205,8 +215,28 @@ def _parse_time_pair(time_type: str, time_values: Union[bool, Sequence[str]]) ->
205215
raise ValidationFailedException(f'time param: {time_type} is not one of "day" or "week"')
206216

207217

208-
def parse_time_arg(key: str = "time") -> List[TimePair]:
209-
return [_parse_time_pair(time_type, time_values) for [time_type, time_values] in _parse_common_multi_arg(key)]
218+
def parse_time_arg(key: str = "time") -> Optional[TimePair]:
219+
time_pairs = [_parse_time_pair(time_type, time_values) for [time_type, time_values] in _parse_common_multi_arg(key)]
220+
221+
# single value
222+
if len(time_pairs) == 0:
223+
return None
224+
if len(time_pairs) == 1:
225+
return time_pairs[0]
226+
227+
# make sure 'day' and 'week' aren't mixed
228+
time_types = set(time_pair.time_type for time_pair in time_pairs)
229+
if len(time_types) >= 2:
230+
raise ValidationFailedException(f'{key}: {time_pairs} mixes "day" and "week" time types')
231+
232+
# merge all time pairs into one
233+
merged = []
234+
for time_pair in time_pairs:
235+
if time_pair.time_values == True:
236+
return time_pair
237+
else:
238+
merged.extend(time_pair.time_values)
239+
return TimePair(time_pairs[0].time_type, merged).to_ranges()
210240

211241

212242
def parse_single_time_arg(key: str) -> TimePair:
@@ -255,25 +285,26 @@ def parse_week_range_arg(key: str) -> Tuple[int, int]:
255285
raise ValidationFailedException(f"{key} must match YYYYWW-YYYYWW")
256286
return r
257287

258-
def parse_day_or_week_arg(key: str, default_value: Optional[int] = None) -> Tuple[int, bool]:
288+
def parse_day_or_week_arg(key: str, default_value: Optional[int] = None) -> TimePair:
259289
v = request.values.get(key)
260290
if not v:
261291
if default_value is not None:
262-
return default_value, guess_time_value_is_day(default_value)
292+
time_type = "day" if guess_time_value_is_day(default_value) else "week"
293+
return TimePair(time_type, [default_value])
263294
raise ValidationFailedException(f"{key} param is required")
264295
# format is either YYYY-MM-DD or YYYYMMDD or YYYYMM
265-
is_week = len(v) == 6
296+
is_week = guess_time_value_is_week(v)
266297
if is_week:
267-
return parse_week_arg(key), False
268-
return parse_day_arg(key), True
298+
return TimePair("week", [parse_week_arg(key)])
299+
return TimePair("day", [parse_day_arg(key)])
269300

270-
def parse_day_or_week_range_arg(key: str) -> Tuple[Tuple[int, int], bool]:
301+
def parse_day_or_week_range_arg(key: str) -> TimePair:
271302
v = request.values.get(key)
272303
if not v:
273304
raise ValidationFailedException(f"{key} param is required")
274305
# format is either YYYY-MM-DD--YYYY-MM-DD or YYYYMMDD-YYYYMMDD or YYYYMM-YYYYMM
275306
# so if the first before the - has length 6, it must be a week
276-
is_week = len(v.split('-', 2)[0]) == 6
307+
is_week = guess_time_value_is_week(v.split('-', 2)[0])
277308
if is_week:
278-
return parse_week_range_arg(key), False
279-
return parse_day_range_arg(key), True
309+
return TimePair("week", [parse_week_range_arg(key)])
310+
return TimePair("day", [parse_day_range_arg(key)])

src/server/_query.py

+24-37
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
from ._db import metadata
2121
from ._printer import create_printer, APrinter
2222
from ._exceptions import DatabaseErrorException
23-
from ._validate import DateRange, extract_strings
23+
from ._validate import extract_strings
2424
from ._params import GeoPair, SourceSignalPair, TimePair
25-
from .utils import time_values_to_ranges, days_to_ranges, weeks_to_ranges
25+
from .utils import time_values_to_ranges, TimeValues
2626

2727

2828
def date_string(value: int) -> str:
@@ -36,7 +36,7 @@ def date_string(value: int) -> str:
3636

3737
def to_condition(
3838
field: str,
39-
value: Union[Tuple[str, str], str, Tuple[int, int], int],
39+
value: Union[str, Tuple[int, int], int],
4040
param_key: str,
4141
params: Dict[str, Any],
4242
formatter=lambda x: x,
@@ -52,7 +52,7 @@ def to_condition(
5252

5353
def filter_values(
5454
field: str,
55-
values: Optional[Sequence[Union[Tuple[str, str], str, Tuple[int, int], int]]],
55+
values: Optional[Sequence[Union[str, Tuple[int, int], int]]],
5656
param_key: str,
5757
params: Dict[str, Any],
5858
formatter=lambda x: x,
@@ -68,7 +68,7 @@ def filter_values(
6868

6969
def filter_strings(
7070
field: str,
71-
values: Optional[Sequence[Union[Tuple[str, str], str]]],
71+
values: Optional[Sequence[str]],
7272
param_key: str,
7373
params: Dict[str, Any],
7474
):
@@ -86,7 +86,7 @@ def filter_integers(
8686

8787
def filter_dates(
8888
field: str,
89-
values: Optional[Sequence[Union[Tuple[int, int], int]]],
89+
values: Optional[TimeValues],
9090
param_key: str,
9191
params: Dict[str, Any],
9292
):
@@ -171,32 +171,29 @@ def filter_pair(pair: SourceSignalPair, i) -> str:
171171
return f"({' OR '.join(parts)})"
172172

173173

174-
def filter_time_pairs(
174+
def filter_time_pair(
175175
type_field: str,
176176
time_field: str,
177-
values: Sequence[TimePair],
177+
pair: Optional[TimePair],
178178
param_key: str,
179179
params: Dict[str, Any],
180180
) -> str:
181181
"""
182-
returns the SQL sub query to filter by the given time pairs
182+
returns the SQL sub query to filter by the given time pair
183183
"""
184-
185-
def filter_pair(pair: TimePair, i) -> str:
186-
type_param = f"{param_key}_{i}t"
187-
params[type_param] = pair.time_type
188-
if isinstance(pair.time_values, bool) and pair.time_values:
189-
return f"{type_field} = :{type_param}"
190-
ranges = weeks_to_ranges(pair.time_values) if pair.is_week else days_to_ranges(pair.time_values)
191-
return f"({type_field} = :{type_param} AND {filter_integers(time_field, cast(Sequence[Union[int, Tuple[int,int]]], ranges), type_param, params)})"
192-
193-
parts = [filter_pair(p, i) for i, p in enumerate(values)]
194-
195-
if not parts:
196-
# something has to be selected
184+
# safety path; should normally not be reached as time pairs are enforced by the API
185+
if not pair:
197186
return "FALSE"
198187

199-
return f"({' OR '.join(parts)})"
188+
type_param = f"{param_key}_0t"
189+
params[type_param] = pair.time_type
190+
if isinstance(pair.time_values, bool) and pair.time_values:
191+
parts = f"{type_field} = :{type_param}"
192+
else:
193+
ranges = pair.to_ranges().time_values
194+
parts = f"({type_field} = :{type_param} AND {filter_integers(time_field, ranges, type_param, params)})"
195+
196+
return f"({parts})"
200197

201198

202199
def parse_row(
@@ -393,7 +390,7 @@ def where(self, **kvargs: Dict[str, Any]) -> "QueryBuilder":
393390
def where_strings(
394391
self,
395392
field: str,
396-
values: Optional[Sequence[Union[Tuple[str, str], str]]],
393+
values: Optional[Sequence[str]],
397394
param_key: Optional[str] = None,
398395
) -> "QueryBuilder":
399396
fq_field = f"{self.alias}.{field}" if "." not in field else field
@@ -413,16 +410,6 @@ def where_integers(
413410
self.conditions.append(filter_integers(fq_field, values, param_key or field, self.params))
414411
return self
415412

416-
def where_dates(
417-
self,
418-
field: str,
419-
values: Optional[Sequence[Union[Tuple[int, int], int]]],
420-
param_key: Optional[str] = None,
421-
) -> "QueryBuilder":
422-
fq_field = self._fq_field(field)
423-
self.conditions.append(filter_dates(fq_field, values, param_key or field, self.params))
424-
return self
425-
426413
def where_geo_pairs(
427414
self,
428415
type_field: str,
@@ -463,17 +450,17 @@ def where_source_signal_pairs(
463450
)
464451
return self
465452

466-
def where_time_pairs(
453+
def where_time_pair(
467454
self,
468455
type_field: str,
469456
value_field: str,
470-
values: Sequence[TimePair],
457+
values: Optional[TimePair],
471458
param_key: Optional[str] = None,
472459
) -> "QueryBuilder":
473460
fq_type_field = self._fq_field(type_field)
474461
fq_value_field = self._fq_field(value_field)
475462
self.conditions.append(
476-
filter_time_pairs(
463+
filter_time_pair(
477464
fq_type_field,
478465
fq_value_field,
479466
values,

src/server/_validate.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from flask import request
44

55
from ._exceptions import UnAuthenticatedException, ValidationFailedException
6+
from .utils import TimeValues
67

78

89
def resolve_auth_token() -> Optional[str]:
@@ -141,14 +142,11 @@ def extract_date(key: Union[str, Sequence[str]]) -> Optional[int]:
141142
return parse_date(s)
142143

143144

144-
DateRange = Union[Tuple[int, int], int]
145-
146-
147-
def extract_dates(key: Union[str, Sequence[str]]) -> Optional[List[DateRange]]:
145+
def extract_dates(key: Union[str, Sequence[str]]) -> Optional[TimeValues]:
148146
parts = extract_strings(key)
149147
if not parts:
150148
return None
151-
values: List[Union[Tuple[int, int], int]] = []
149+
values: TimeValues = []
152150

153151
def push_range(first: str, last: str):
154152
first_d = parse_date(first)

0 commit comments

Comments
 (0)