Source code for cardinal_pythonlib.pyramid.requests

#!/usr/bin/env python
# cardinal_pythonlib/pyramid/requests.py

"""
===============================================================================

    Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of cardinal_pythonlib.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

===============================================================================

Functions to operate on Pyramid requests.

"""

import gzip
import logging
from typing import Generator
import zlib

# noinspection PyUnresolvedReferences
from pyramid.request import Request

# noinspection PyUnresolvedReferences
from webob.headers import EnvironHeaders

try:
    # noinspection PyPackageRequirements
    import brotli  # pip install brotlipy
except ImportError:
    brotli = None

log = logging.getLogger(__name__)


# =============================================================================
# Encoding checks
# =============================================================================

HTTP_ACCEPT_ENCODING = "Accept-Encoding"
HTTP_CONTENT_ENCODING = "Content-Encoding"

# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
BR_ENCODING = "br"
COMPRESS_ENCODING = "compress"
DEFLATE_ENCODING = "deflate"
GZIP_ENCODING = "gzip"
X_GZIP_ENCODING = "x-gzip"
IDENTITY_ENCODING = "identity"


[docs]def gen_accept_encoding_definitions( accept_encoding: str, ) -> Generator[str, None, None]: """ For a given HTTP ``Accept-Encoding`` field value, generate encoding definitions. An example might be: .. code-block:: python from cardinal_pythonlib.pyramid.compression import * accept_encoding = "br;q=1.0, gzip;q=0.8, *;q=0.1" print(list(gen_encoding_definitions(accept_encoding))) which gives .. code-block:: none ['br;q=1.0', 'gzip;q=0.8', '*;q=0.1'] """ for definition in accept_encoding.split(","): yield definition.strip()
[docs]def gen_accept_encodings(accept_encoding: str) -> Generator[str, None, None]: """ For a given HTTP ``Accept-Encoding`` field value, generate encodings. An example might be: .. code-block:: python from cardinal_pythonlib.pyramid.compression import * accept_encoding = "br;q=1.0, gzip;q=0.8, *;q=0.1" print(list(gen_encodings(accept_encoding))) which gives .. code-block:: none ['br', 'gzip', '*'] """ for definition in gen_accept_encoding_definitions(accept_encoding): yield definition.split(";")[0].strip()
[docs]def request_accepts_gzip(request: Request) -> bool: """ Does the request specify an ``Accept-Encoding`` header that includes ``gzip``? Note: - Field names in HTTP headers are case-insensitive (e.g. "accept-encoding" is fine). - WebOb request headers are in a case-insensitive dictionary; see https://docs.pylonsproject.org/projects/webob/en/stable/. (Easily verified by altering the key being checked; it works.) - However, the value is a Python string so is case-sensitive. - But the HTTP standard doesn't say that field values are case-insensitive; see https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2. - So we'll do a case-sensitive check for "gzip". - But there is also a bit of other syntax possible; see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding. """ # noqa headers = request.headers # type: EnvironHeaders if HTTP_ACCEPT_ENCODING not in headers: return False accepted_encodings = headers[HTTP_ACCEPT_ENCODING] for encoding in gen_accept_encodings(accepted_encodings): if encoding == GZIP_ENCODING: return True return False
[docs]def gen_content_encodings(request: Request) -> Generator[str, None, None]: """ Generates content encodings in the order they are specified -- that is, the order in which they were applied. """ headers = request.headers # type: EnvironHeaders if HTTP_CONTENT_ENCODING not in headers: return content_encoding = headers[HTTP_CONTENT_ENCODING] for encoding in content_encoding.split(","): yield encoding.strip()
[docs]def gen_content_encodings_reversed( request: Request, ) -> Generator[str, None, None]: """ Generates content encodings in reverse order -- that is, in the order required to reverse them. """ for encoding in reversed(list(gen_content_encodings(request))): yield encoding
[docs]def decompress_request(request: Request) -> None: """ Reverses anything specified in ``Content-Encoding``, modifying the request in place. """ for encoding in gen_content_encodings_reversed(request): if encoding in [GZIP_ENCODING, X_GZIP_ENCODING]: request.body = gzip.decompress(request.body) elif encoding == IDENTITY_ENCODING: pass elif encoding == DEFLATE_ENCODING: request.body = zlib.decompress(request.body) elif encoding == BR_ENCODING: if brotli: # https://python-hyper.org/projects/brotlipy/en/latest/ # noinspection PyUnresolvedReferences request.body = brotli.decompress(request.body) else: raise NotImplementedError( f"Content-Encoding {encoding} not supported " f"(brotlipy package not installed)" ) else: raise ValueError(f"Unknown Content-Encoding: {encoding}")
# ... e.g. "compress"; LZW; patent expired; see # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding # noqa