def __contains__(self, item: str) -> bool: """ Verify if a string match a member or an attribute name of an Header. """ if item in self.attrs: return True item = normalize_str(item) for attr in self.attrs: target = normalize_str(attr) if item == target or item in header_content_split(target, " "): return True return False
def __isub__(self, other: Union[Header, str]) -> "Headers": """ Inline subtract, using the operator '-'. If str is subtracted to it, would be looking for header named like provided str. Would remove any entries named 'Set-Cookies'. eg : >>> headers = Header("Set-Cookies", "HELLO=WORLD") + Header("Allow", "POST") >>> headers.has("Set-Cookies") True >>> headers -= 'Set-Cookies' >>> headers.has("Set-Cookies") False """ if isinstance(other, str): other_normalized = normalize_str(other) to_be_removed = list() for header in self: if other_normalized == header.normalized_name: to_be_removed.append(header) for header in to_be_removed: self._headers.remove(header) return self if isinstance(other, Header): if other in self: self._headers.remove(other) return self raise TypeError('Cannot subtract type "{type_}" to Headers.'.format( type_=str(type(other))))
def __setitem__(self, key: str, value: str) -> None: """ Set header using the bracket syntax. This operation would remove any existing header named after the key. Warning, if your value contain comma separated entries, it will split it into multiple Header instance. >>> headers = Headers() >>> headers.content_type = "application/json" >>> len(headers) 1 >>> headers.accept = "text/html, application/json;q=1.0" >>> len(headers) 3 """ if not isinstance(value, str): raise TypeError( "Cannot assign header '{key}' using type {type_} to headers.". format(key=key, type_=type(value))) if key in self: del self[key] # Permit to detect multiple entries. if normalize_str(key) != "subject": entries: List[str] = header_content_split(value, ",") if len(entries) > 1: for entry in entries: self._headers.append(Header(key, entry)) return self._headers.append(Header(key, value))
def __isub__(self, other: str) -> "Header": """ This method should allow you to remove attribute or member from header. """ if not isinstance(other, str): raise TypeError("You cannot subtract {type_} to an Header.".format( type_=str(type(other)))) if other not in self: raise ValueError( "You cannot subtract '{element}' from '{header_name}' Header because its not there." .format(element=other, header_name=self.pretty_name)) other = normalize_str(other) if other in self._valued_attrs: del self[other] if other in self._not_valued_attrs: self._not_valued_attrs.remove(other) while True: try: self._not_valued_attrs.remove(other) except ValueError: break for elem in findall( r"{member_name}(?=[;\n])".format( member_name=escape(other)), self._content + "\n", IGNORECASE, ): self._content = header_strip(self._content, elem) return self
def __dir__(self) -> Iterable[str]: """ Provide a better auto-completion when using Python interpreter. We are feeding __dir__ so Python can be aware of what properties are callable. In other words, more precise auto-completion when not using IDE. """ return list(super().__dir__()) + [ normalize_str(key) for key in self._valued_attrs.keys() ]
def __contains__(self, item: Union[Header, str]) -> bool: """ This method will allow you to test if a header, based on its string name, is present or not in headers. You could also use a Header object to verify it's presence. """ item = normalize_str(item) if isinstance(item, str) else item for header in self: if isinstance(item, str) and header.normalized_name == item: return True if isinstance(item, Header) and header == item: return True return False
def __init__(self, name: str, content: str): """ :param name: The name of the header, should contain only ASCII characters with no spaces in it. :param content: Initial content associated with the header. """ if not is_legal_header_name(name): raise ValueError( f"'{name}' is not a valid header name. Cannot proceed with it." ) self._name: str = name self._normalized_name: str = normalize_str(self._name) self._pretty_name: str = prettify_header_name(self._name) self._content: str = content self._members: List[str] = header_content_split(self._content, ";") self._not_valued_attrs: List[str] = list() self._valued_attrs: MutableMapping[str, Union[ str, List[str]]] = CaseInsensitiveDict() for member in self._members: if member == "": continue if "=" in member: key, value = tuple(member.split("=", maxsplit=1)) # avoid confusing base64 look alike single value for (key, value) if value.count("=") == len(value) or len( value) == 0 or " " in key: self._not_valued_attrs.append(unquote(member)) continue if key not in self._valued_attrs: self._valued_attrs[key] = value else: if isinstance(self._valued_attrs[key], str): self._valued_attrs[key] = [ self._valued_attrs[key], value ] # type: ignore else: self._valued_attrs[key].append(value) # type: ignore continue self._not_valued_attrs.append(unquote(member))
def __delattr__(self, item: str) -> None: """ Remove any attribute named after the key in header using the property notation. >>> headers = Header("Content-Type", "text/html; charset=UTF-8") + Header("Vary", "Content-Type") >>> repr(headers.content_type) 'Content-Type: text/html; charset=UTF-8' >>> del headers.content_type.charset >>> repr(headers.content_type) 'Content-Type: text/html' """ item = normalize_str(item) if item not in self._valued_attrs: raise AttributeError( "'{item}' attribute is not defined within '{header}' header.". format(item=item, header=self.name)) del self[item]
def index(self, __value: Union[Header, str], __start: int = 0, __stop: int = -1) -> int: """ Search for the first appearance of an header based on its name or instance in Headers. Same method signature as list().index(). Raises IndexError if not found. >>> headers = Header("A", "hello") + Header("B", "world") + Header("C", "funny; riddle") >>> headers.index("A") 0 >>> headers.index("A", 1) Traceback (most recent call last): ... IndexError: Value 'A' is not present within Headers. >>> headers.index("A", 0, 1) 0 >>> headers.index("C") 2 >>> headers.index(headers[0]) 0 >>> headers.index(headers[1]) 1 """ value_is_header: bool = isinstance(__value, Header) normalized_value: Optional[str] = normalize_str( __value # type: ignore ) if not value_is_header else None headers_len: int = len(self) # Convert indices to positive indices __start = __start % headers_len if __start < 0 else __start __stop = __stop % headers_len if __stop < 0 else __stop for header, index in zip(self._headers[__start:__stop + 1], range(__start, __stop + 1)): if value_is_header and __value == header: return index elif normalized_value == header.normalized_name: return index raise IndexError(f"Value '{__value}' is not present within Headers.")
def __getitem__(self, item: Union[str, int]) -> Union[Header, List[Header]]: """ Extract header using the bracket syntax, dict-like. The result is either a single Header or a list of Header. """ if isinstance(item, int): return self._headers[item] item = normalize_str(item) if item not in self: raise KeyError( "'{item}' header is not defined in headers.".format(item=item) ) headers: List[Header] = list() for header in self._headers: if header.normalized_name == item: headers.append(header) return headers if len(headers) > 1 or OUTPUT_LOCK_TYPE else headers.pop()
def __delitem__(self, key: str) -> None: """ Remove all matching header named after called key. >>> headers = Header("Content-Type", "text/html") + Header("Allow", "POST") >>> headers.has("Content-Type") True >>> del headers['content-type'] >>> headers.has("Content-Type") False """ key = normalize_str(key) to_be_removed = [] if key not in self: raise KeyError( "'{item}' header is not defined in headers.".format(item=key)) for header in self: if header.normalized_name == key: to_be_removed.append(header) for header in to_be_removed: self._headers.remove(header)
def __delitem__(self, key: str) -> None: del self._store[normalize_str(key)]
def __getitem__(self, key: str) -> Any: return self._store[normalize_str(key)][1]
def __setitem__(self, key: str, value: Any) -> None: # Use the lowercased key for lookups, but store the actual # key alongside the value. self._store[normalize_str(key)] = (key, value)
def parse_it(raw_headers: Any) -> Headers: """ Just decode anything that could contain headers. That simple PERIOD. :param raw_headers: Accept bytes, str, fp, dict, email.Message, requests.Response, urllib3.HTTPResponse and httpx.Response. :raises: TypeError: If passed argument cannot be parsed to extract headers from it. """ headers: Optional[Iterable[Tuple[str, Any]]] = None if isinstance(raw_headers, str): headers = HeaderParser().parsestr(raw_headers, headersonly=True).items() elif isinstance(raw_headers, bytes) or isinstance(raw_headers, RawIOBase): decoded, not_decoded = extract_encoded_headers( raw_headers if isinstance(raw_headers, bytes ) else raw_headers.read() or b"") return parse_it(decoded) elif isinstance(raw_headers, Mapping) or isinstance(raw_headers, Message): headers = raw_headers.items() else: r = extract_class_name(type(raw_headers)) if r: if r == "requests.models.Response": headers = [] for header_name in raw_headers.raw.headers: for header_content in raw_headers.raw.headers.getlist( header_name): headers.append((header_name, header_content)) elif r in [ "httpx._models.Response", "urllib3.response.HTTPResponse" ]: headers = raw_headers.headers.items() if headers is None: raise TypeError( "Cannot parse type {type_} as it is not supported by kiss-header.". format(type_=type(raw_headers))) revised_headers: List[Tuple[str, str]] = decode_partials(headers) # Sometime raw content does not begin with headers. If that is the case, search for the next line. if (len(revised_headers) == 0 and len(raw_headers) > 0 and (isinstance(raw_headers, bytes) or isinstance(raw_headers, str))): next_iter = raw_headers.split( b"\n" if isinstance(raw_headers, bytes) else "\n", maxsplit=1 # type: ignore ) if len(next_iter) >= 2: return parse_it(next_iter[-1]) # Prepare Header objects list_of_headers: List[Header] = [] for head, content in revised_headers: # We should ignore when a illegal name is considered as an header. We avoid ValueError (in __init__ of Header) if is_legal_header_name(head) is False: continue entries: List[str] = header_content_split(content, ",") # Multiple entries are detected in one content at the only exception that its not IMAP header "Subject". if len(entries) > 1 and normalize_str(head) != "subject": for entry in entries: list_of_headers.append(Header(head, entry)) else: list_of_headers.append(Header(head, content)) return Headers(*list_of_headers)