Skip to content

Commit 789fda2

Browse files
committed
Move parse methods from covidcast
1 parent 7774f96 commit 789fda2

31 files changed

+374
-350
lines changed

src/server/_params.py

+175
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from ._exceptions import ValidationFailedException
1010
from .utils import days_in_range, weeks_in_range, guess_time_value_is_day, guess_time_value_is_week, IntRange, TimeValues, days_to_ranges, weeks_to_ranges
11+
from ._validate import require_any, require_all
1112

1213

1314
def _parse_common_multi_arg(key: str) -> List[Tuple[str, Union[bool, Sequence[str]]]]:
@@ -308,3 +309,177 @@ def parse_day_or_week_range_arg(key: str) -> TimeSet:
308309
if is_week:
309310
return TimeSet("week", [parse_week_range_arg(key)])
310311
return TimeSet("day", [parse_day_range_arg(key)])
312+
313+
314+
def _extract_value(key: Union[str, Sequence[str]]) -> Optional[str]:
315+
if isinstance(key, str):
316+
return request.values.get(key)
317+
for k in key:
318+
if k in request.values:
319+
return request.values[k]
320+
return None
321+
322+
323+
def _extract_list_value(key: Union[str, Sequence[str]]) -> List[str]:
324+
if isinstance(key, str):
325+
return request.values.getlist(key)
326+
for k in key:
327+
if k in request.values:
328+
return request.values.getlist(k)
329+
return []
330+
331+
332+
def extract_strings(key: Union[str, Sequence[str]]) -> Optional[List[str]]:
333+
s = _extract_list_value(key)
334+
if not s:
335+
# nothing to do
336+
return None
337+
# we can have multiple values
338+
return [v for vs in s for v in vs.split(",")]
339+
340+
341+
def extract_integer(key: Union[str, Sequence[str]]) -> Optional[int]:
342+
s = _extract_value(key)
343+
if not s:
344+
# nothing to do
345+
return None
346+
try:
347+
return int(s)
348+
except ValueError:
349+
raise ValidationFailedException(f"{key}: not a number: {s}")
350+
351+
352+
def extract_integers(key: Union[str, Sequence[str]]) -> Optional[List[IntRange]]:
353+
parts = extract_strings(key)
354+
if not parts:
355+
# nothing to do
356+
return None
357+
358+
def _parse_range(part: str):
359+
if "-" not in part:
360+
return int(part)
361+
r = part.split("-", 2)
362+
first = int(r[0])
363+
last = int(r[1])
364+
if first == last:
365+
# the first and last numbers are the same, just treat it as a singe value
366+
return first
367+
elif last > first:
368+
# add the range as an array
369+
return (first, last)
370+
# the range is inverted, this is an error
371+
raise ValidationFailedException(f"{key}: the given range is inverted")
372+
373+
try:
374+
values = [_parse_range(part) for part in parts]
375+
# check for invalid values
376+
return None if any(v is None for v in values) else values
377+
except ValueError as e:
378+
raise ValidationFailedException(f"{key}: not a number: {str(e)}")
379+
380+
381+
def parse_date(s: str) -> int:
382+
# parses a given string in format YYYYMMDD or YYYY-MM-DD to a number in the form YYYYMMDD
383+
try:
384+
return int(s.replace("-", ""))
385+
except ValueError:
386+
raise ValidationFailedException(f"not a valid date: {s}")
387+
388+
389+
def extract_date(key: Union[str, Sequence[str]]) -> Optional[int]:
390+
s = _extract_value(key)
391+
if not s:
392+
return None
393+
return parse_date(s)
394+
395+
396+
def extract_dates(key: Union[str, Sequence[str]]) -> Optional[TimeValues]:
397+
parts = extract_strings(key)
398+
if not parts:
399+
return None
400+
values: TimeValues = []
401+
402+
def push_range(first: str, last: str):
403+
first_d = parse_date(first)
404+
last_d = parse_date(last)
405+
if first_d == last_d:
406+
# the first and last numbers are the same, just treat it as a singe value
407+
return first_d
408+
if last_d > first_d:
409+
# add the range as an array
410+
return (first_d, last_d)
411+
# the range is inverted, this is an error
412+
raise ValidationFailedException(f"{key}: the given range is inverted")
413+
414+
for part in parts:
415+
if "-" not in part and ":" not in part:
416+
# YYYYMMDD
417+
values.append(parse_date(part))
418+
continue
419+
if ":" in part:
420+
# YYYY-MM-DD:YYYY-MM-DD
421+
range_part = part.split(":", 2)
422+
r = push_range(range_part[0], range_part[1])
423+
if r is None:
424+
return None
425+
values.append(r)
426+
continue
427+
# YYYY-MM-DD or YYYYMMDD-YYYYMMDD
428+
# split on the dash
429+
range_part = part.split("-")
430+
if len(range_part) == 2:
431+
# YYYYMMDD-YYYYMMDD
432+
r = push_range(range_part[0], range_part[1])
433+
if r is None:
434+
return None
435+
values.append(r)
436+
continue
437+
# YYYY-MM-DD
438+
values.append(parse_date(part))
439+
# success, return the list
440+
return values
441+
442+
def parse_source_signal_sets() -> List[SourceSignalSet]:
443+
ds = request.values.get("data_source")
444+
if ds:
445+
# old version
446+
require_any("signal", "signals", empty=True)
447+
signals = extract_strings(("signals", "signal"))
448+
if len(signals) == 1 and signals[0] == "*":
449+
return [SourceSignalSet(ds, True)]
450+
return [SourceSignalSet(ds, signals)]
451+
452+
if ":" not in request.values.get("signal", ""):
453+
raise ValidationFailedException("missing parameter: signal or (data_source and signal[s])")
454+
455+
return parse_source_signal_arg()
456+
457+
458+
def parse_geo_sets() -> List[GeoSet]:
459+
geo_type = request.values.get("geo_type")
460+
if geo_type:
461+
# old version
462+
require_any("geo_value", "geo_values", empty=True)
463+
geo_values = extract_strings(("geo_values", "geo_value"))
464+
if len(geo_values) == 1 and geo_values[0] == "*":
465+
return [GeoSet(geo_type, True)]
466+
return [GeoSet(geo_type, geo_values)]
467+
468+
if ":" not in request.values.get("geo", ""):
469+
raise ValidationFailedException("missing parameter: geo or (geo_type and geo_value[s])")
470+
471+
return parse_geo_arg()
472+
473+
474+
def parse_time_set() -> TimeSet:
475+
time_type = request.values.get("time_type")
476+
if time_type:
477+
# old version
478+
require_all("time_type", "time_values")
479+
time_values = extract_dates("time_values")
480+
return TimeSet(time_type, time_values)
481+
482+
if ":" not in request.values.get("time", ""):
483+
raise ValidationFailedException("missing parameter: time or (time_type and time_values)")
484+
485+
return parse_time_arg()

src/server/_query.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
from ._common import db
1919
from ._printer import create_printer, APrinter
2020
from ._exceptions import DatabaseErrorException
21-
from ._validate import extract_strings
22-
from ._params import GeoSet, SourceSignalSet, TimeSet
21+
from ._params import extract_strings, GeoSet, SourceSignalSet, TimeSet
2322
from .utils import time_values_to_ranges, IntRange, TimeValues
2423

2524

src/server/_validate.py

-129
Original file line numberDiff line numberDiff line change
@@ -55,132 +55,3 @@ def require_any(*values: str, empty=False) -> bool:
5555
if request.values.get(value) or (empty and value in request.values):
5656
return True
5757
raise ValidationFailedException(f"missing parameter: need one of [{', '.join(values)}]")
58-
59-
60-
def _extract_value(key: Union[str, Sequence[str]]) -> Optional[str]:
61-
if isinstance(key, str):
62-
return request.values.get(key)
63-
for k in key:
64-
if k in request.values:
65-
return request.values[k]
66-
return None
67-
68-
69-
def _extract_list_value(key: Union[str, Sequence[str]]) -> List[str]:
70-
if isinstance(key, str):
71-
return request.values.getlist(key)
72-
for k in key:
73-
if k in request.values:
74-
return request.values.getlist(k)
75-
return []
76-
77-
78-
def extract_strings(key: Union[str, Sequence[str]]) -> Optional[List[str]]:
79-
s = _extract_list_value(key)
80-
if not s:
81-
# nothing to do
82-
return None
83-
# we can have multiple values
84-
return [v for vs in s for v in vs.split(",")]
85-
86-
87-
def extract_integer(key: Union[str, Sequence[str]]) -> Optional[int]:
88-
s = _extract_value(key)
89-
if not s:
90-
# nothing to do
91-
return None
92-
try:
93-
return int(s)
94-
except ValueError:
95-
raise ValidationFailedException(f"{key}: not a number: {s}")
96-
97-
98-
def extract_integers(key: Union[str, Sequence[str]]) -> Optional[List[IntRange]]:
99-
parts = extract_strings(key)
100-
if not parts:
101-
# nothing to do
102-
return None
103-
104-
def _parse_range(part: str):
105-
if "-" not in part:
106-
return int(part)
107-
r = part.split("-", 2)
108-
first = int(r[0])
109-
last = int(r[1])
110-
if first == last:
111-
# the first and last numbers are the same, just treat it as a singe value
112-
return first
113-
elif last > first:
114-
# add the range as an array
115-
return (first, last)
116-
# the range is inverted, this is an error
117-
raise ValidationFailedException(f"{key}: the given range is inverted")
118-
119-
try:
120-
values = [_parse_range(part) for part in parts]
121-
# check for invalid values
122-
return None if any(v is None for v in values) else values
123-
except ValueError as e:
124-
raise ValidationFailedException(f"{key}: not a number: {str(e)}")
125-
126-
127-
def parse_date(s: str) -> int:
128-
# parses a given string in format YYYYMMDD or YYYY-MM-DD to a number in the form YYYYMMDD
129-
try:
130-
return int(s.replace("-", ""))
131-
except ValueError:
132-
raise ValidationFailedException(f"not a valid date: {s}")
133-
134-
135-
def extract_date(key: Union[str, Sequence[str]]) -> Optional[int]:
136-
s = _extract_value(key)
137-
if not s:
138-
return None
139-
return parse_date(s)
140-
141-
142-
def extract_dates(key: Union[str, Sequence[str]]) -> Optional[TimeValues]:
143-
parts = extract_strings(key)
144-
if not parts:
145-
return None
146-
values: TimeValues = []
147-
148-
def push_range(first: str, last: str):
149-
first_d = parse_date(first)
150-
last_d = parse_date(last)
151-
if first_d == last_d:
152-
# the first and last numbers are the same, just treat it as a singe value
153-
return first_d
154-
if last_d > first_d:
155-
# add the range as an array
156-
return (first_d, last_d)
157-
# the range is inverted, this is an error
158-
raise ValidationFailedException(f"{key}: the given range is inverted")
159-
160-
for part in parts:
161-
if "-" not in part and ":" not in part:
162-
# YYYYMMDD
163-
values.append(parse_date(part))
164-
continue
165-
if ":" in part:
166-
# YYYY-MM-DD:YYYY-MM-DD
167-
range_part = part.split(":", 2)
168-
r = push_range(range_part[0], range_part[1])
169-
if r is None:
170-
return None
171-
values.append(r)
172-
continue
173-
# YYYY-MM-DD or YYYYMMDD-YYYYMMDD
174-
# split on the dash
175-
range_part = part.split("-")
176-
if len(range_part) == 2:
177-
# YYYYMMDD-YYYYMMDD
178-
r = push_range(range_part[0], range_part[1])
179-
if r is None:
180-
return None
181-
values.append(r)
182-
continue
183-
# YYYY-MM-DD
184-
values.append(parse_date(part))
185-
# success, return the list
186-
return values

src/server/endpoints/afhsb.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from flask import Blueprint
44

55
from .._config import AUTH
6+
from .._params import extract_integers, extract_strings
67
from .._query import execute_queries, filter_integers, filter_strings
7-
from .._validate import check_auth_token, extract_integers, extract_strings, require_all
8+
from .._validate import check_auth_token, require_all
89

910
# first argument is the endpoint name
1011
bp = Blueprint("afhsb", __name__)

src/server/endpoints/cdc.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from flask import Blueprint
22

33
from .._config import AUTH, NATION_REGION, REGION_TO_STATE
4-
from .._validate import require_all, extract_strings, extract_integers, check_auth_token
4+
from .._params import extract_strings, extract_integers
55
from .._query import filter_strings, execute_queries, filter_integers
6+
from .._validate import require_all, check_auth_token
67

78
# first argument is the endpoint name
89
bp = Blueprint("cdc", __name__)

src/server/endpoints/covid_hosp_facility.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from flask import Blueprint
22

3+
from .._params import extract_integers, extract_strings
34
from .._query import execute_query, QueryBuilder
4-
from .._validate import extract_integers, extract_strings, require_all
5+
from .._validate import require_all
56

67
# first argument is the endpoint name
78
bp = Blueprint("covid_hosp_facility", __name__)

src/server/endpoints/covid_hosp_facility_lookup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from flask import Blueprint
22

3+
from .._params import extract_strings
34
from .._query import execute_query, QueryBuilder
4-
from .._validate import extract_strings, require_any
5+
from .._validate import require_any
56

67
# first argument is the endpoint name
78
bp = Blueprint("covid_hosp_facility_lookup", __name__)

src/server/endpoints/covid_hosp_state_timeseries.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from flask import Blueprint
22

3+
from .._params import extract_integers, extract_strings, extract_date
34
from .._query import execute_query, QueryBuilder
4-
from .._validate import extract_integers, extract_strings, extract_date, require_all
5+
from .._validate import require_all
56

67
# first argument is the endpoint name
78
bp = Blueprint("covid_hosp_state_timeseries", __name__)

0 commit comments

Comments
 (0)