216 lines
7.2 KiB
Python
216 lines
7.2 KiB
Python
import string
|
|
from typing import Any, Dict, List, Optional, Union
|
|
|
|
from ._exceptions import ParseError, ValidationError
|
|
|
|
## Grammar
|
|
|
|
|
|
HTAB = "\t"
|
|
SP = " "
|
|
obs_text = "".join(chr(i) for i in range(0x80, 0xFF + 1)) # 0x80-0xFF
|
|
|
|
tchar = "!#$%&'*+-.^_`|~0123456789" + string.ascii_letters
|
|
qdtext = "".join(
|
|
[
|
|
HTAB,
|
|
SP,
|
|
"\x21",
|
|
"".join(chr(i) for i in range(0x23, 0x5B + 1)), # 0x23-0x5b
|
|
"".join(chr(i) for i in range(0x5D, 0x7E + 1)), # 0x5D-0x7E
|
|
obs_text,
|
|
]
|
|
)
|
|
|
|
TIME_FIELDS = [
|
|
"max_age",
|
|
"max_stale",
|
|
"min_fresh",
|
|
"s_maxage",
|
|
]
|
|
|
|
BOOLEAN_FIELDS = [
|
|
"immutable",
|
|
"must_revalidate",
|
|
"must_understand",
|
|
"no_store",
|
|
"no_transform",
|
|
"only_if_cached",
|
|
"public",
|
|
"proxy_revalidate",
|
|
]
|
|
|
|
LIST_FIELDS = ["no_cache", "private"]
|
|
|
|
__all__ = (
|
|
"CacheControl",
|
|
"Vary",
|
|
)
|
|
|
|
|
|
def strip_ows_around(text: str) -> str:
|
|
return text.strip(" ").strip("\t")
|
|
|
|
|
|
def normalize_directive(text: str) -> str:
|
|
return text.replace("-", "_")
|
|
|
|
|
|
def parse_cache_control(cache_control_values: List[str]) -> "CacheControl":
|
|
directives = {}
|
|
|
|
for cache_control_value in cache_control_values:
|
|
if "no-cache=" in cache_control_value or "private=" in cache_control_value:
|
|
cache_control_splited = [cache_control_value]
|
|
else:
|
|
cache_control_splited = cache_control_value.split(",")
|
|
|
|
for directive in cache_control_splited:
|
|
key: str = ""
|
|
value: Optional[str] = None
|
|
dquote = False
|
|
|
|
if not directive:
|
|
raise ParseError("The directive should not be left blank.")
|
|
|
|
directive = strip_ows_around(directive)
|
|
|
|
if not directive:
|
|
raise ParseError("The directive should not contain only whitespaces.")
|
|
|
|
for i, key_char in enumerate(directive):
|
|
if key_char == "=":
|
|
value = directive[i + 1 :]
|
|
|
|
if not value:
|
|
raise ParseError("The directive value cannot be left blank.")
|
|
|
|
if value[0] == '"':
|
|
dquote = True
|
|
if dquote and value[-1] != '"':
|
|
raise ParseError("Invalid quotes around the value.")
|
|
|
|
if not dquote:
|
|
for value_char in value:
|
|
if value_char not in tchar:
|
|
raise ParseError(
|
|
f"The character '{value_char!r}' is not permitted for the unquoted values."
|
|
)
|
|
else:
|
|
for value_char in value[1:-1]:
|
|
if value_char not in qdtext:
|
|
raise ParseError(
|
|
f"The character '{value_char!r}' is not permitted for the quoted values."
|
|
)
|
|
break
|
|
|
|
if key_char not in tchar:
|
|
raise ParseError(f"The character '{key_char!r}' is not permitted in the directive name.")
|
|
key += key_char
|
|
directives[key] = value
|
|
validated_data = CacheControl.validate(directives)
|
|
return CacheControl(**validated_data)
|
|
|
|
|
|
class Vary:
|
|
def __init__(self, values: List[str]) -> None:
|
|
self._values = values
|
|
|
|
@classmethod
|
|
def from_value(cls, vary_values: List[str]) -> "Vary":
|
|
values = []
|
|
|
|
for vary_value in vary_values:
|
|
for field_name in vary_value.split(","):
|
|
field_name = field_name.strip()
|
|
values.append(field_name)
|
|
return Vary(values)
|
|
|
|
|
|
class CacheControl:
|
|
def __init__(
|
|
self,
|
|
immutable: bool = False, # [RFC8246]
|
|
max_age: Optional[int] = None, # [RFC9111, Section 5.2.1.1, 5.2.2.1]
|
|
max_stale: Optional[int] = None, # [RFC9111, Section 5.2.1.2]
|
|
min_fresh: Optional[int] = None, # [RFC9111, Section 5.2.1.3]
|
|
must_revalidate: bool = False, # [RFC9111, Section 5.2.2.2]
|
|
must_understand: bool = False, # [RFC9111, Section 5.2.2.3]
|
|
no_cache: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.1.4, 5.2.2.4]
|
|
no_store: bool = False, # [RFC9111, Section 5.2.1.5, 5.2.2.5]
|
|
no_transform: bool = False, # [RFC9111, Section 5.2.1.6, 5.2.2.6]
|
|
only_if_cached: bool = False, # [RFC9111, Section 5.2.1.7]
|
|
private: Union[bool, List[str]] = False, # [RFC9111, Section 5.2.2.7]
|
|
proxy_revalidate: bool = False, # [RFC9111, Section 5.2.2.8]
|
|
public: bool = False, # [RFC9111, Section 5.2.2.9]
|
|
s_maxage: Optional[int] = None, # [RFC9111, Section 5.2.2.10]
|
|
) -> None:
|
|
self.immutable = immutable
|
|
self.max_age = max_age
|
|
self.max_stale = max_stale
|
|
self.min_fresh = min_fresh
|
|
self.must_revalidate = must_revalidate
|
|
self.must_understand = must_understand
|
|
self.no_cache = no_cache
|
|
self.no_store = no_store
|
|
self.no_transform = no_transform
|
|
self.only_if_cached = only_if_cached
|
|
self.private = private
|
|
self.proxy_revalidate = proxy_revalidate
|
|
self.public = public
|
|
self.s_maxage = s_maxage
|
|
|
|
@classmethod
|
|
def validate(cls, directives: Dict[str, Any]) -> Dict[str, Any]:
|
|
validated_data: Dict[str, Any] = {}
|
|
|
|
for key, value in directives.items():
|
|
key = normalize_directive(key)
|
|
if key in TIME_FIELDS:
|
|
if value is None:
|
|
raise ValidationError(f"The directive '{key}' necessitates a value.")
|
|
|
|
if value[0] == '"' or value[-1] == '"':
|
|
raise ValidationError(f"The argument '{key}' should be an integer, but a quote was found.")
|
|
|
|
try:
|
|
validated_data[key] = int(value)
|
|
except Exception:
|
|
raise ValidationError(f"The argument '{key}' should be an integer, but got '{value!r}'.")
|
|
elif key in BOOLEAN_FIELDS:
|
|
if value is not None:
|
|
raise ValidationError(f"The directive '{key}' should have no value, but it does.")
|
|
validated_data[key] = True
|
|
elif key in LIST_FIELDS:
|
|
if value is None:
|
|
validated_data[key] = True
|
|
else:
|
|
values = []
|
|
for list_value in value[1:-1].split(","):
|
|
if not list_value:
|
|
raise ValidationError("The list value must not be empty.")
|
|
list_value = strip_ows_around(list_value)
|
|
values.append(list_value)
|
|
validated_data[key] = values
|
|
|
|
return validated_data
|
|
|
|
def __repr__(self) -> str:
|
|
fields = ""
|
|
|
|
for key in TIME_FIELDS:
|
|
key = key.replace("-", "_")
|
|
value = getattr(self, key)
|
|
if value:
|
|
fields += f"{key}={value}, "
|
|
|
|
for key in BOOLEAN_FIELDS:
|
|
key = key.replace("-", "_")
|
|
value = getattr(self, key)
|
|
if value:
|
|
fields += f"{key}, "
|
|
|
|
fields = fields[:-2]
|
|
|
|
return f"<{type(self).__name__} {fields}>"
|