Coverage for /builds/BuildGrid/buildgrid/buildgrid/server/operations/filtering/sanitizer.py: 95.45%
66 statements
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
« prev ^ index » next coverage.py v7.4.1, created at 2024-10-04 17:48 +0000
1# Copyright (C) 2020 Bloomberg LP
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# <http://www.apache.org/licenses/LICENSE-2.0>
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
16import re
17from abc import ABC
18from datetime import datetime
19from typing import Any, List
21import dateutil.parser
22from dateutil.tz import tzutc
24import buildgrid.server.enums as enums
25from buildgrid.server.exceptions import InvalidArgumentError
26from buildgrid.server.operations.filtering import SortKey
29class ValueSanitizer(ABC):
30 """Base sanitizer class."""
32 @property
33 def valid_values(self) -> List[str]:
34 """Return a list of valid values for the sanitizer.
36 This is only useful for sanitizers with a finite list of valid values.
38 Raises NotImplementedError for sanitizers which aren't based on an
39 enum of valid values.
41 """
42 raise NotImplementedError()
44 # TODO probably make this generic?
45 def sanitize(self, value_string: str) -> Any:
46 """Method that takes an input string, validates that input string,
47 and transforms it to a value of another type if necessary.
49 Raises InvalidArgumentError if the sanitization fails. Returns a
50 value of an arbitrary type if it succeeds."""
51 raise NotImplementedError()
54class RegexValueSanitizer(ValueSanitizer):
55 """Sanitizer for regexable patterns."""
57 def __init__(self, filter_name: str, regex_pattern: str) -> None:
58 self.filter_name = filter_name
59 self.regex = re.compile(f"^({regex_pattern})$")
61 def sanitize(self, value_string: str) -> str:
62 if not self.regex.fullmatch(value_string):
63 raise InvalidArgumentError(f"[{value_string}] is not a valid value for {self.filter_name}.")
64 return value_string
67class DatetimeValueSanitizer(ValueSanitizer):
68 """Sanitizer for ISO 8601 datetime strings."""
70 def __init__(self, filter_name: str) -> None:
71 self.filter_name = filter_name
73 def sanitize(self, value_string: str) -> datetime:
74 try:
75 dt = dateutil.parser.isoparse(value_string)
76 # Convert to UTC and remove timezone awareness
77 if dt.tzinfo:
78 dt = dt.astimezone(tz=tzutc()).replace(tzinfo=None)
79 return dt
80 except ValueError:
81 raise InvalidArgumentError(f"[{value_string}] is not a valid value for {self.filter_name}.")
84class OperationStageValueSanitizer(ValueSanitizer):
85 """Sanitizer for the OperationStage type.
87 Matches valid OperationStage values and converts to the
88 numeric representation of that stage."""
90 def __init__(self, filter_name: str) -> None:
91 self.filter_name = filter_name
93 @property
94 def valid_values(self) -> List[str]:
95 return [stage.name for stage in enums.OperationStage]
97 def sanitize(self, value_string: str) -> int:
98 try:
99 stage = value_string.upper()
100 return enums.OperationStage[stage].value
101 except KeyError:
102 raise InvalidArgumentError(f"[{value_string}] is not a valid value for {self.filter_name}.")
105class SortKeyValueSanitizer(ValueSanitizer):
106 """Sanitizer for sort orders.
108 Produces a SortKey tuple, which specifies both a key name and a boolean
109 indicating ascending/descending order."""
111 def __init__(self, filter_name: str) -> None:
112 self.filter_name = filter_name
114 def sanitize(self, value_string: str) -> SortKey:
115 desc_key = "(desc)"
116 asc_key = "(asc)"
118 key_name = value_string.lower().strip()
119 if not key_name:
120 raise InvalidArgumentError(f"Invalid sort key [{value_string}].")
121 descending = False
123 if value_string.endswith(desc_key):
124 descending = True
125 key_name = key_name[: -len(desc_key)].strip()
126 if not key_name:
127 raise InvalidArgumentError(f"Invalid sort key [{value_string}].")
129 elif value_string.endswith(asc_key):
130 key_name = key_name[: -len(asc_key)].strip()
131 if not key_name:
132 raise InvalidArgumentError(f"Invalid sort key [{value_string}].")
134 return SortKey(name=key_name, descending=descending)