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}>"