pax_global_header00006660000000000000000000000064144071210650014512gustar00rootroot0000000000000052 comment=282ede0484c4fa26fb5e8fcd504ce71faefb6ea5 django-compression-middleware-0.5.0/000077500000000000000000000000001440712106500174305ustar00rootroot00000000000000django-compression-middleware-0.5.0/.github/000077500000000000000000000000001440712106500207705ustar00rootroot00000000000000django-compression-middleware-0.5.0/.github/workflows/000077500000000000000000000000001440712106500230255ustar00rootroot00000000000000django-compression-middleware-0.5.0/.github/workflows/main.yml000066400000000000000000000015141440712106500244750ustar00rootroot00000000000000name: Django compression middleware on: - push - pull_request jobs: build: runs-on: ubuntu-18.04 strategy: matrix: python: - "2.7" - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - pypy-2.7 - pypy-3.6 - pypy-3.7 - pypy-3.8 - pypy-3.9 steps: - uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} - name: Install tox and any other packages run: pip install tox tox-gh-actions - name: Run tox # Run with whatever python interpreter is available on the current image run: tox --skip-missing-interpreters true django-compression-middleware-0.5.0/.gitignore000066400000000000000000000004121440712106500214150ustar00rootroot00000000000000*.py[cod] __pycache__ # Packages *.egg *.egg-info dist build eggs parts sdist develop-eggs .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml htmlcov .pytest_cache # Editor files *~ *.swp *.log *.bak *.backup django-compression-middleware-0.5.0/.travis.yml000066400000000000000000000062711440712106500215470ustar00rootroot00000000000000dist: xenial language: python cache: pip python: - "2.7" - "3.4" - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" - "3.10" - "3.11" - "pypy" # 2.7 - "pypy3.5" - "pypy3" # 3.6 env: - DJANGO_VERSION=1.11 - DJANGO_VERSION=2.0 - DJANGO_VERSION=2.1 - DJANGO_VERSION=2.2 - DJANGO_VERSION=3.0 - DJANGO_VERSION=3.1 - DJANGO_VERSION=3.2 - DJANGO_VERSION=4.0 - DJANGO_VERSION=4.1 matrix: exclude: - python: "2.7" env: DJANGO_VERSION=2.0 - python: "2.7" env: DJANGO_VERSION=2.1 - python: "2.7" env: DJANGO_VERSION=2.2 - python: "2.7" env: DJANGO_VERSION=3.0 - python: "2.7" env: DJANGO_VERSION=3.1 - python: "2.7" env: DJANGO_VERSION=3.2 - python: "2.7" env: DJANGO_VERSION=4.0 - python: "2.7" env: DJANGO_VERSION=4.1 - python: "pypy" env: DJANGO_VERSION=2.0 - python: "pypy" env: DJANGO_VERSION=2.1 - python: "pypy" env: DJANGO_VERSION=2.2 - python: "pypy" env: DJANGO_VERSION=3.0 - python: "pypy" env: DJANGO_VERSION=3.1 - python: "pypy" env: DJANGO_VERSION=3.2 - python: "pypy" env: DJANGO_VERSION=4.0 - python: "pypy" env: DJANGO_VERSION=4.1 - python: "3.4" env: DJANGO_VERSION=1.11 - python: "3.4" env: DJANGO_VERSION=2.1 - python: "3.4" env: DJANGO_VERSION=2.2 - python: "3.4" env: DJANGO_VERSION=3.0 - python: "3.5" env: DJANGO_VERSION=1.11 - python: "3.5" env: DJANGO_VERSION=3.0 - python: "3.5" env: DJANGO_VERSION=3.1 - python: "3.5" env: DJANGO_VERSION=3.2 - python: "3.5" env: DJANGO_VERSION=4.0 - python: "3.5" env: DJANGO_VERSION=4.1 - python: "3.6" env: DJANGO_VERSION=1.11 - python: "3.6" env: DJANGO_VERSION=4.0 - python: "3.6" env: DJANGO_VERSION=4.1 - python: "3.7" env: DJANGO_VERSION=1.11 - python: "3.7" env: DJANGO_VERSION=4.0 - python: "3.7" env: DJANGO_VERSION=4.1 - python: "3.8" env: DJANGO_VERSION=1.11 - python: "3.8" env: DJANGO_VERSION=2.0 - python: "3.8" env: DJANGO_VERSION=2.1 - python: "pypy3.5" env: DJANGO_VERSION=1.11 - python: "pypy3.5" env: DJANGO_VERSION=3.0 - python: "pypy3.5" env: DJANGO_VERSION=3.1 - python: "pypy3.5" env: DJANGO_VERSION=3.2 - python: "pypy3.5" env: DJANGO_VERSION=4.0 - python: "pypy3.5" env: DJANGO_VERSION=4.1 - python: "pypy3" env: DJANGO_VERSION=1.11 - python: "pypy3" env: DJANGO_VERSION=4.0 - python: "pypy3" env: DJANGO_VERSION=4.1 # command to install dependencies install: - pip install -q Django~=$DJANGO_VERSION - pip install -q -r requirements_dev.txt # command to run tests script: - pytest - python setup.py build - python setup.py install django-compression-middleware-0.5.0/LICENSE000066400000000000000000000405251440712106500204430ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. django-compression-middleware-0.5.0/MANIFEST.in000066400000000000000000000003431440712106500211660ustar00rootroot00000000000000include LICENSE include README.rst include requirements*.txt include tox.ini include pytest.ini recursive-include compression_middleware *.html *py recursive-include tests *py global-exclude __pycache__ global-exclude *.py[co] django-compression-middleware-0.5.0/README.rst000066400000000000000000000117311440712106500211220ustar00rootroot00000000000000=========================================================================== Django Compression Middleware =========================================================================== This middleware implements compressed content encoding for HTTP. It is similar to Django's ``GZipMiddleware`` (`documentation`_), but additionally supports other compression methods. It is meant to be a drop-in replacement for Django's ``GZipMiddleware``. Its documentation — including security warnings — therefore apply here as well. The middleware is focussed on the task of compressing typical Django responses such as HTML, JSON, etc. Both normal (bulk) and streaming responses are supported. For static file compression, have a look at other projects such as `WhiteNoise`_. Zstandard is a new method for compression with little client support so far. Most browsers now support Brotli compression (check support status on `Can I use... Brotli`_). The middleware will choose the best compression method supported by the client as indicated in the request's ``Accept-Encoding`` header. In order of preference: - Zstandard (zstd) - Brotli (br) - gzip (gzip) Summary of the project status: * .. image:: https://img.shields.io/github/actions/workflow/status/friedelwolff/django-compression-middleware/main.yml :target: https://github.com/friedelwolff/django-compression-middleware/actions * .. image:: https://img.shields.io/pypi/djversions/django-compression-middleware.svg * .. image:: https://img.shields.io/pypi/pyversions/django-compression-middleware.svg * .. image:: https://img.shields.io/pypi/implementation/django-compression-middleware.svg .. _`documentation`: https://docs.djangoproject.com/en/dev/ref/middleware/#module-django.middleware.gzip .. _`WhiteNoise`: https://whitenoise.readthedocs.io/ .. _`Can I use... Brotli`: http://caniuse.com/#search=brotli Installation and usage ---------------------- The following requirements are supported and tested in all reasonable combinations: - Python versions: 2.7, 3.5–3.11 - Interpreters: CPython and PyPy. - Django versions: 1.11–4.1 .. code:: shell pip install --upgrade django-compression-middleware To apply compression to all the views served by Django, add ``compression_middleware.middleware.CompressionMiddleware`` to the ``MIDDLEWARE`` setting: .. code:: python MIDDLEWARE = [ # ... 'compression_middleware.middleware.CompressionMiddleware', # ... ] Remove ``GZipMiddleware`` and ``BrotliMiddleware`` if you used it before. Consult the Django documentation on the correct `ordering of middleware`_. .. _`ordering of middleware`: https://docs.djangoproject.com/en/dev/ref/middleware/#middleware-ordering Alternatively you can decorate views individually to serve them with compression: .. code:: python from compression_middleware.decorators import compress_page @compress_page def index_view(request): ... Note that your browser might not send the ``br`` entry in the ``Accept-Encoding`` header when you test without HTTPS (common on localhost). You can force it to send the header, though. In Firefox, visit ``about:config`` and set ``network.http.accept-encoding`` to indicate support. Note that you might encounter some problems on the web with such a setting (which is why Brotli is only supported on secure connections by default). Credits and Resources --------------------- The code and tests in this project are based on Django's ``GZipMiddleware`` and Vašek Dohnal's ``django-brotli``. For compression, it uses the following modules to bind to fast C modules: - The `zstandard`_ bindings. It supports both a C module (for CPython) and CFFI which should be appropriate for PyPy. See the documentation for full details. - The `Brotli`_ bindings or `brotlipy`_. The latter is preferred on PyPy since it is implemented using cffi. But both should work on both Python implementations. - Python's builtin `gzip`_ module. .. _zstandard: https://pypi.org/project/zstandard/ .. _Brotli: https://pypi.org/project/Brotli/ .. _brotlipy: https://pypi.org/project/brotlipy/ .. _gzip: https://docs.python.org/3/library/gzip.html Further readding on Wikipedia: - `HTTP compression `__ - `Zstandard `__ - `Brotli `__ - `gzip `__ Contributing ------------ 1. Clone this repository (``git clone ...``) 2. Create a virtualenv 3. Install package dependencies: ``pip install --upgrade -r requirements_dev.txt`` 4. Change some code 5. Run the tests: in the project root simply execute ``pytest``, and afterwards preferably ``tox`` to test the full test matrix. Consider installing as many supported interpreters as possible (having them in your ``PATH`` is often sufficient). 6. Submit a pull request and check for any errors reported by the Continuous Integration service. License ------- The MPL 2.0 License Copyright (c) 2019-2023 `Friedel Wolff `_. django-compression-middleware-0.5.0/compression_middleware/000077500000000000000000000000001440712106500241665ustar00rootroot00000000000000django-compression-middleware-0.5.0/compression_middleware/__init__.py000066400000000000000000000000001440712106500262650ustar00rootroot00000000000000django-compression-middleware-0.5.0/compression_middleware/br.py000066400000000000000000000015151440712106500251450ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. __all__ = ["brotli_compress", "brotli_compress_stream"] from brotli import compress, Compressor DEFAULT_LEVEL = 4 def brotli_compress(content): return compress(content, quality=DEFAULT_LEVEL) def brotli_compress_stream(sequence): yield b"" compressor = Compressor(quality=DEFAULT_LEVEL) try: # Brotli bindings process = compressor.process except AttributeError: # brotlipy process = compressor.compress for item in sequence: out = process(item) if out: yield out out = compressor.finish() if out: yield out django-compression-middleware-0.5.0/compression_middleware/decorators.py000066400000000000000000000007631440712106500267130ustar00rootroot00000000000000# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. __all__ = ["compress_page"] from .middleware import CompressionMiddleware from django.utils.decorators import decorator_from_middleware compress_page = decorator_from_middleware(CompressionMiddleware) compress_page.__doc__ = "Decorator to compress the view response if the client supports it." django-compression-middleware-0.5.0/compression_middleware/middleware.py000066400000000000000000000126001440712106500266540ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # # Based on Django's gzip middleware # - Copyright (c) Django Software Foundation and individual contributors. # - 3-clause BSD # and django-brotli # - Copyright (c) 2016–2017 Vašek Dohnal (@illagrenan) # - MIT Licence __all__ = ["CompressionMiddleware"] from .br import brotli_compress, brotli_compress_stream from .zstd import zstd_compress, zstd_compress_stream from django.middleware.gzip import ( compress_string as gzip_compress, compress_sequence as gzip_compress_stream, ) from django.utils.cache import patch_vary_headers try: from django.utils.deprecation import MiddlewareMixin except ImportError: # pragma: no cover MiddlewareMixin = object # Minimum response length before we'll consider compression. Small responses # won't necessarily be smaller after compression, and we want to save at least # enough to make the time expended worthwhile. Since MTUs around 1500 are # common, and HTTP headers are often more than 500 bytes (more so if there # are cookies), we guess that responses smaller than 500 bytes is likely to fit # in the MTU (or not) mostly due to other factors, not compression. MIN_LEN = 500 # The compression has to reduce the length, otherwise we're just fooling # around. Since we'll have to add the Content-Encoding header, we need to # make that addition worthwhile, too. So the compressed response must be # smaller by some margin. This value should be at least 24 which is # len("Content-Encoding: gzip\r\n"), but a bigger value could reflect that a # non-trivial improvement in transfer time is required to make up for the time # required for decompression. An improvement of a few bytes is unlikely to # actually reduce the network communication in terms of MTUs. MIN_IMPROVEMENT = 100 # supported encodings in order of preference # (encoding, bulk_compressor, stream_compressor) compressors = ( ("zstd", zstd_compress, zstd_compress_stream), ("br", brotli_compress, brotli_compress_stream), ("gzip", gzip_compress, gzip_compress_stream), ) def encoding_name(s): """Obtain 'br' out of ' br;q=0.5' or similar.""" # We won't break if the ordering is specified with q=, but we ignore it. # Only a quality level of 0 is honoured -- in such a case we handle it as # if the encoding wasn't specified at all. if ";" in s: s, q = s.split(";", 1) if "=" in q: _, q = q.split("=", 1) try: q = float(q) if q == 0.0: return None except ValueError: pass return s.strip() def compressor(accept_encoding): # We don't want to process extremely long headers. It might be an attack: accept_encoding = accept_encoding[:200] client_encodings = set(encoding_name(e) for e in accept_encoding.split(",")) if "*" in client_encodings: # Our first choice: return compressors[0] for encoding, compress_func, stream_func in compressors: if encoding in client_encodings: return (encoding, compress_func, stream_func) return (None, None, None) class CompressionMiddleware(MiddlewareMixin): """ This middleware compresses content based on the Accept-Encoding header. The Vary header is set for the sake of downstream caches. """ def process_response(self, request, response): # Test a few things before we even try: # - content is already encoded # - really short responses are not worth it if response.has_header("Content-Encoding") or ( not response.streaming and len(response.content) < MIN_LEN ): return response patch_vary_headers(response, ("Accept-Encoding",)) ae = request.META.get("HTTP_ACCEPT_ENCODING", "") encoding, compress_func, stream_func = compressor(ae) if not encoding: # No compression in common with client (the client probably didn't # indicate support for anything). return response if response.streaming: # Delete the `Content-Length` header for streaming content, because # we won't know the compressed size until we stream it. response.streaming_content = stream_func(response.streaming_content) del response["Content-Length"] else: #TODO: protect against excessive response size compressed_content = compress_func(response.content) # Return the compressed content only if compression is worth it if len(compressed_content) >= len(response.content) - MIN_IMPROVEMENT: return response response.content = compressed_content response["Content-Length"] = str(len(response.content)) # If there is a strong ETag, make it weak to fulfill the requirements # of RFC 7232 section-2.1 while also allowing conditional request # matches on ETags. # Django's ConditionalGetMiddleware relies upon this etag behaviour. etag = response.get("ETag") if etag and etag.startswith('"'): response["ETag"] = "W/" + etag response["Content-Encoding"] = encoding return response django-compression-middleware-0.5.0/compression_middleware/zstd.py000066400000000000000000000015541440712106500255310ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. __all__ = ["zstd_compress", "zstd_compress_stream"] from django.utils.text import StreamingBuffer import zstandard as zstd DEFAULT_LEVEL = 7 def zstd_compress(content): cctx = zstd.ZstdCompressor(level=DEFAULT_LEVEL) return cctx.compress(content) def zstd_compress_stream(sequence): buf = StreamingBuffer() cctx = zstd.ZstdCompressor(level=DEFAULT_LEVEL) with cctx.stream_writer(buf, write_return_read=False) as compressor: yield buf.read() for item in sequence: if compressor.write(item): yield buf.read() compressor.flush(zstd.FLUSH_FRAME) yield buf.read() django-compression-middleware-0.5.0/doc/000077500000000000000000000000001440712106500201755ustar00rootroot00000000000000django-compression-middleware-0.5.0/doc/faq.rst000066400000000000000000000076451440712106500215120ustar00rootroot00000000000000========= Questions ========= - What about the `BREACH attack`_? Django provides mitigations against the BREACH attack since version 1.10. For additional protection, you can consider ``django-debreach`` and rate limiting. If you are using the builtin CSRF mechanisms (i.e. not roling your own), you are probably safe as far as BREACH is concerned. .. _BREACH attack: http://breachattack.com/ - Why not implement each possible encoding as its own middleware and apply them in order? While this is a clean design, it suffers from a few shortcomings. It requires developers / system administrators to add multiple middlewares for a single concern, and it might duplicate some functionality, such as parsing the ``Accept-Encoding`` header. - Why not let the web server handle the compression? It is a good idea to let the web server handle the compression, as it might be more optimised for it, and it frees up your application resources for handling the application. This middleware is appropriate for cases where the webserver is not able to do it or does not support the latest compression algorithms. Moreover, compression in middleware unlocks the possibility of caching compressed pages which might be beneficial in some cases. - What about streaming responses? Just like ``GZipMiddleware``, streaming responses are supported, and the compressed data is streamed as it becomes available from the compressor. - What about compression with the deflate algorithm? The deflate algorithm provides very little benefit over gzip in terms of compression — it is actually the same algorithm, but using 11 bytes less overhead. Due to historical incompatibilities most people prefer gzip over deflate. There is no realistic chance today of a client supporting deflate that doesn't also support gzip. - What about compression with the SDCH algorithm? (Or bzip2, lma, xz...) These are not in IANA's `content coding registry`_ and are not widely supported by clients. Zstandard is supported, though. .. _content coding registry: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding - Does this provide any real value over Django's ``GZipMiddleware``? Brotli promises better compression using less CPU time, and fast decompression. It is now widely supported in browsers. For small responses the effects of any compression algorithm is likely to be small. For a small web response of a few KB Brotli is unlikely to need less MTUs than gzip. It should use slightly less CPU time for compression and decompression than gzip, but the difference will be small. In terms of total time in the request-response cycle over the internet, you are unlikely to save more than a millisecond. For larger responses, the effects become more pronounced, and you might benefit in the order of multiple milliseconds compared to gzip. Naturally this all depends heavily on the content of the response, the server and client CPUs and the attributes of the network connection. Django compression middleware tries to choose parameters so that using Brotli should usually use less CPU time and result in a smaller response than with gzip. If you specifically target very slow or very fast connections, a slight tweak to the compression level might provide a slightly better balance of priorities. - Isn't compression of small responses a waste of time? It could well be. Django compression middleware addresses this in two ways: - We check if the response after compression is smaller by some margin. If it isn't, the uncompressed response is sent. - If the response is very small before compression, it is sent uncompressed. All of this means that users and system administrators should benefit regardless of whether they are on fast or slow connections or computers. The benefit in any specific case might be small, but you should benefit in aggregate spread over all your responses. django-compression-middleware-0.5.0/pytest.ini000066400000000000000000000002101440712106500214520ustar00rootroot00000000000000[pytest] norecursedirs = .git .tox venv* requirements* log python_files = test_*.py addopts = tests/ --verbose --color=yes --showlocals django-compression-middleware-0.5.0/requirements.txt000066400000000000000000000002601440712106500227120ustar00rootroot00000000000000# Requirements required by this package: brotlipy>=0.7.0 ; platform_python_implementation == 'PyPy' Brotli ; platform_python_implementation != 'PyPy' zstandard Django>=1.11.0 django-compression-middleware-0.5.0/requirements_dev.txt000066400000000000000000000002011440712106500235430ustar00rootroot00000000000000# Requirements required by this package: -r requirements.txt # Requirements needed for development & build & testing pytest tox django-compression-middleware-0.5.0/setup.cfg000066400000000000000000000001331440712106500212460ustar00rootroot00000000000000[metadata] license_files = LICENSE.txt [wheel] universal = 1 [bdist_wheel] universal = 1 django-compression-middleware-0.5.0/setup.py000066400000000000000000000037771440712106500211600ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- import io from platform import python_implementation from setuptools import setup install_requires = ['django', 'zstandard'] if python_implementation() == 'PyPy': install_requires.append('brotlipy') else: install_requires.append('Brotli') setup( name='django-compression-middleware', version='0.5.0', description="""Django middleware to compress responses using several algorithms.""", long_description=io.open("README.rst", 'r', encoding="utf-8").read(), url='https://github.com/friedelwolff/django-compression-middleware', author='Friedel Wolff', author_email='friedel@translate.org.za', packages=['compression_middleware'], install_requires=install_requires, include_package_data=True, zip_safe=True, classifiers=[ 'Development Status :: 4 - Beta', 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', 'Framework :: Django :: 4.1', 'Intended Audience :: Developers', 'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware', ] ) django-compression-middleware-0.5.0/tests/000077500000000000000000000000001440712106500205725ustar00rootroot00000000000000django-compression-middleware-0.5.0/tests/__init__.py000066400000000000000000000001351440712106500227020ustar00rootroot00000000000000# -*- encoding: utf-8 -*- from django.conf import settings settings.configure(DEBUG=False) django-compression-middleware-0.5.0/tests/test_decorator.py000066400000000000000000000044361440712106500241740ustar00rootroot00000000000000# -*- encoding: utf-8 -*- from __future__ import unicode_literals import brotli from django.http import HttpRequest, HttpResponse, StreamingHttpResponse from django.test import RequestFactory, SimpleTestCase, TestCase from compression_middleware.decorators import compress_page class CompressPageDecoratorTest(SimpleTestCase): compressible_string = b"a" * 500 sequence = [b"a" * 500, b"b" * 200, b"a" * 300] sequence_unicode = ["a" * 500, "é" * 200, "a" * 300] request_factory = RequestFactory() def setUp(self): self.req = self.request_factory.get("/") self.req.META["HTTP_ACCEPT_ENCODING"] = "gzip, deflate, br" self.req.META[ "HTTP_USER_AGENT" ] = "Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1" self.resp = HttpResponse() self.resp.status_code = 200 self.resp.content = self.compressible_string self.resp["Content-Type"] = "text/html; charset=UTF-8" self.stream_resp = StreamingHttpResponse(self.sequence) self.stream_resp["Content-Type"] = "text/html; charset=UTF-8" self.stream_resp_unicode = StreamingHttpResponse(self.sequence_unicode) self.stream_resp_unicode["Content-Type"] = "text/html; charset=UTF-8" def test_small_page(self): @compress_page def a_small_view(request): return HttpResponse() r = a_small_view(self.req) self.assertFalse(r.has_header("Content-Encoding")) self.assertEqual(r.content, b"") def test_normal_page(self): @compress_page def a_view(request): return self.resp r = a_view(self.req) self.assertEqual(r.get("Content-Encoding"), "br") self.assertEqual(r.get("Content-Length"), str(len(r.content))) self.assertTrue(brotli.decompress(r.content), self.compressible_string) def test_streaming_page(self): @compress_page def a_streaming_view(request): return self.stream_resp_unicode r = a_streaming_view(self.req) self.assertEqual(r.get("Content-Encoding"), "br") self.assertFalse(r.has_header("Content-Length")) self.assertEqual( brotli.decompress(b"".join(r)), b"".join(x.encode("utf-8") for x in self.sequence_unicode) ) django-compression-middleware-0.5.0/tests/test_interactions.py000066400000000000000000000210241440712106500247040ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import unicode_literals # This tests if the middleware works as Django expects from its own # GZipMiddleware. This way we test the expected interactions with other parts # of Django, such as ConditionalGetMiddleware, which relies on certain parts # of the compression middleware, such as e-tags. # This is mostly unchanged from the Django test suite with the middleware # imported as GZipMiddleware for testing. import gzip import random from io import BytesIO from django.http import ( FileResponse, HttpResponse, StreamingHttpResponse, ) from compression_middleware.middleware import CompressionMiddleware as GZipMiddleware from django.middleware.http import ConditionalGetMiddleware from django.test import RequestFactory, SimpleTestCase, override_settings try: # Python 2 range = xrange int2byte = chr except NameError: # Python 3 import struct int2byte = struct.Struct(">B").pack class GZipMiddlewareTest(SimpleTestCase): """ Tests the GZipMiddleware. """ short_string = b"This string is too short to be worth compressing." compressible_string = b"a" * 500 incompressible_string = b"".join( int2byte(random.randint(0, 255)) for _ in range(500) ) sequence = [b"a" * 500, b"b" * 200, b"a" * 300] sequence_unicode = ["a" * 500, "é" * 200, "a" * 300] request_factory = RequestFactory() def setUp(self): self.req = self.request_factory.get("/") self.req.META["HTTP_ACCEPT_ENCODING"] = "gzip, deflate" self.req.META[ "HTTP_USER_AGENT" ] = "Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1" self.resp = HttpResponse() self.resp.status_code = 200 self.resp.content = self.compressible_string self.resp["Content-Type"] = "text/html; charset=UTF-8" def get_response(self, request): return self.resp @staticmethod def decompress(gzipped_string): with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f: return f.read() @staticmethod def get_mtime(gzipped_string): with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f: f.read() # must read the data before accessing the header return f.mtime def test_compress_response(self): """ Compression is performed on responses with compressible content. """ r = GZipMiddleware(self.get_response)(self.req) self.assertEqual(self.decompress(r.content), self.compressible_string) self.assertEqual(r.get("Content-Encoding"), "gzip") self.assertEqual(r.get("Content-Length"), str(len(r.content))) def test_compress_streaming_response(self): """ Compression is performed on responses with streaming content. """ def get_stream_response(request): resp = StreamingHttpResponse(self.sequence) resp["Content-Type"] = "text/html; charset=UTF-8" return resp r = GZipMiddleware(get_stream_response)(self.req) self.assertEqual(self.decompress(b"".join(r)), b"".join(self.sequence)) self.assertEqual(r.get("Content-Encoding"), "gzip") self.assertFalse(r.has_header("Content-Length")) def test_compress_streaming_response_unicode(self): """ Compression is performed on responses with streaming Unicode content. """ def get_stream_response_unicode(request): resp = StreamingHttpResponse(self.sequence_unicode) resp["Content-Type"] = "text/html; charset=UTF-8" return resp r = GZipMiddleware(get_stream_response_unicode)(self.req) self.assertEqual( self.decompress(b"".join(r)), b"".join(x.encode("utf-8") for x in self.sequence_unicode), ) self.assertEqual(r.get("Content-Encoding"), "gzip") self.assertFalse(r.has_header("Content-Length")) def test_compress_file_response(self): """ Compression is performed on FileResponse. """ with open(__file__, "rb") as file1: def get_response(req): file_resp = FileResponse(file1) file_resp["Content-Type"] = "text/html; charset=UTF-8" return file_resp r = GZipMiddleware(get_response)(self.req) with open(__file__, "rb") as file2: self.assertEqual(self.decompress(b"".join(r)), file2.read()) self.assertEqual(r.get("Content-Encoding"), "gzip") self.assertIsNot(r.file_to_stream, file1) def test_compress_non_200_response(self): """ Compression is performed on responses with a status other than 200 (#10762). """ self.resp.status_code = 404 r = GZipMiddleware(self.get_response)(self.req) self.assertEqual(self.decompress(r.content), self.compressible_string) self.assertEqual(r.get("Content-Encoding"), "gzip") def test_no_compress_short_response(self): """ Compression isn't performed on responses with short content. """ self.resp.content = self.short_string r = GZipMiddleware(self.get_response)(self.req) self.assertEqual(r.content, self.short_string) self.assertIsNone(r.get("Content-Encoding")) def test_no_compress_compressed_response(self): """ Compression isn't performed on responses that are already compressed. """ self.resp["Content-Encoding"] = "deflate" r = GZipMiddleware(self.get_response)(self.req) self.assertEqual(r.content, self.compressible_string) self.assertEqual(r.get("Content-Encoding"), "deflate") def test_no_compress_incompressible_response(self): """ Compression isn't performed on responses with incompressible content. """ self.resp.content = self.incompressible_string r = GZipMiddleware(self.get_response)(self.req) self.assertEqual(r.content, self.incompressible_string) self.assertIsNone(r.get("Content-Encoding")) def test_compress_deterministic(self): """ Compression results are the same for the same content and don't include a modification time (since that would make the results of compression non-deterministic and prevent ConditionalGetMiddleware from recognizing conditional matches on gzipped content). """ r1 = GZipMiddleware(self.get_response)(self.req) r2 = GZipMiddleware(self.get_response)(self.req) self.assertEqual(r1.content, r2.content) self.assertEqual(self.get_mtime(r1.content), 0) self.assertEqual(self.get_mtime(r2.content), 0) @override_settings(USE_ETAGS=True) class ETagGZipMiddlewareTest(SimpleTestCase): """ ETags are handled properly by GZipMiddleware. """ rf = RequestFactory() compressible_string = b"a" * 500 def test_strong_etag_modified(self): """ GZipMiddleware makes a strong ETag weak. """ def get_response(req): response = HttpResponse(self.compressible_string) response["ETag"] = '"eggs"' return response request = self.rf.get("/", HTTP_ACCEPT_ENCODING="gzip, deflate") gzip_response = GZipMiddleware(get_response)(request) self.assertEqual(gzip_response["ETag"], 'W/"eggs"') def test_weak_etag_not_modified(self): """ GZipMiddleware doesn't modify a weak ETag. """ def get_response(req): response = HttpResponse(self.compressible_string) response["ETag"] = 'W/"eggs"' return response request = self.rf.get("/", HTTP_ACCEPT_ENCODING="gzip, deflate") gzip_response = GZipMiddleware(get_response)(request) self.assertEqual(gzip_response["ETag"], 'W/"eggs"') def test_etag_match(self): """ GZipMiddleware allows 304 Not Modified responses. """ def get_response(req): return HttpResponse(self.compressible_string) def get_cond_response(req): return ConditionalGetMiddleware(get_response)(req) request = self.rf.get("/", HTTP_ACCEPT_ENCODING="gzip, deflate") response = GZipMiddleware(get_cond_response)(request) gzip_etag = response["ETag"] next_request = self.rf.get( "/", HTTP_ACCEPT_ENCODING="gzip, deflate", HTTP_IF_NONE_MATCH=gzip_etag ) next_response = ConditionalGetMiddleware(get_response)(next_request) self.assertEqual(next_response.status_code, 304) django-compression-middleware-0.5.0/tests/test_middleware.py000066400000000000000000000251741440712106500243310ustar00rootroot00000000000000# -*- encoding: utf-8 -*- # partially based on tests in django and django-brotli from io import BytesIO import gzip import random from unittest import TestCase import brotli import zstandard as zstd from django.http import ( FileResponse, HttpResponse, StreamingHttpResponse, ) from django.middleware.gzip import GZipMiddleware from django.test import RequestFactory, SimpleTestCase try: # Python 2 range = xrange int2byte = chr except NameError: # Python 3 import struct int2byte = struct.Struct(">B").pack from compression_middleware.middleware import CompressionMiddleware, compressor from .utils import UTF8_LOREM_IPSUM_IN_CZECH class FakeRequestAcceptsZstd(object): META = { "HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch, br, zstd" } class FakeRequestAcceptsBrotli(object): META = { "HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch, br" } class InvalidAcceptEcondingRequest(object): META = { "HTTP_ACCEPT_ENCODING": "text/plain,*/*; charset=utf-8" } class FakeLegacyRequest(object): META = { } def gzip_decompress(gzipped_string): with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f: return f.read() class FakeResponse(object): streaming = False def __init__(self, content, headers=None, streaming=None): self.content = content.encode(encoding="utf-8") self.headers = headers or {} if streaming: self.streaming = streaming def has_header(self, header): return header in self.headers def get(self, key): return self.headers.get(key, None) def __getitem__(self, header): return self.headers[header] def __setitem__(self, header, value): self.headers[header] = value class MiddlewareTestCase(TestCase): def test_middleware_compress_response(self): fake_request = FakeRequestAcceptsBrotli() response_content = UTF8_LOREM_IPSUM_IN_CZECH fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) decompressed_response = brotli.decompress(response.content) # type: bytes self.assertEqual( response_content, decompressed_response.decode(encoding="utf-8") ) self.assertEqual(response.get("Vary"), "Accept-Encoding") def test_middleware_compress_response_zstd(self): fake_request = FakeRequestAcceptsZstd() response_content = UTF8_LOREM_IPSUM_IN_CZECH fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) cctx = zstd.ZstdDecompressor() decompressed_response = cctx.decompress(response.content) # type: bytes self.assertEqual( response_content, decompressed_response.decode(encoding="utf-8") ) self.assertEqual(response.get("Vary"), "Accept-Encoding") def test_etag_is_updated_if_present(self): fake_request = FakeRequestAcceptsBrotli() response_content = UTF8_LOREM_IPSUM_IN_CZECH * 5 fake_etag_content = '"foo"' fake_response = FakeResponse( content=response_content, headers={"ETag": fake_etag_content} ) self.assertEqual(fake_response["ETag"], fake_etag_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) decompressed_response = brotli.decompress(response.content) # type: bytes self.assertEqual(response_content, decompressed_response.decode(encoding="utf-8")) # note: this is where we differ from django-brotli # django-brotli's expectation: ### self.assertEqual(response["ETag"], '"foo;br\\"') # Django's expectation: self.assertEqual(response["ETag"], 'W/"foo"') def test_middleware_wont_compress_response_if_response_is_small(self): fake_request = FakeRequestAcceptsBrotli() response_content = "Hello World" self.assertLess(len(response_content), 200) # a < b fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) self.assertEqual( response_content, response.content.decode(encoding="utf-8") ) self.assertFalse(response.has_header("Vary")) self.assertEqual(response.get("Content-Encoding"), None) def test_middleware_wont_compress_if_client_not_accept(self): fake_request = FakeLegacyRequest() response_content = UTF8_LOREM_IPSUM_IN_CZECH fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) django_gzip_middleware = GZipMiddleware(lambda: fake_response) gzip_response = django_gzip_middleware.process_response( fake_request, fake_response ) self.assertEqual( response_content, response.content.decode(encoding="utf-8") ) self.assertEqual(response.get("Vary"), "Accept-Encoding") self.assertEqual(response.get("Content-Encoding"), None) def test_middleware_wont_compress_if_invalid_header(self): """ Test that the middleware doesn't crash if the client sends an invalid Accept-Encoding header. """ fake_request = InvalidAcceptEcondingRequest() response_content = UTF8_LOREM_IPSUM_IN_CZECH fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) response = compression_middleware.process_response( fake_request, fake_response ) django_gzip_middleware = GZipMiddleware(lambda: fake_response) gzip_response = django_gzip_middleware.process_response( fake_request, fake_response ) self.assertEqual( response_content, response.content.decode(encoding="utf-8") ) self.assertEqual( gzip_response.content.decode(encoding="utf-8"), response.content.decode(encoding="utf-8") ) self.assertEqual(response.get("Vary"), "Accept-Encoding") self.assertEqual(response.get("Content-Encoding"), None) def test_middleware_wont_compress_if_response_is_already_compressed(self): fake_request = FakeRequestAcceptsBrotli() response_content = UTF8_LOREM_IPSUM_IN_CZECH fake_response = FakeResponse(content=response_content) compression_middleware = CompressionMiddleware(lambda: fake_response) django_gzip_middleware = GZipMiddleware(lambda: fake_response) gzip_response = django_gzip_middleware.process_response( fake_request, fake_response ) response = compression_middleware.process_response( fake_request, gzip_response ) self.assertEqual( response_content, gzip_decompress(response.content).decode(encoding="utf-8") ) self.assertEqual(response.get("Vary"), "Accept-Encoding") def test_content_encoding_parsing(self): self.assertEqual(compressor("")[0], None) self.assertEqual(compressor("gzip")[0], "gzip") self.assertEqual(compressor("br")[0], "br") self.assertEqual(compressor("gzip, br")[0], "br") self.assertEqual(compressor("br;q=1.0, gzip;q=0.8")[0], "br") self.assertEqual(compressor("br;q=0, gzip;q=0.8")[0], "gzip") self.assertEqual(compressor("bla;bla;gzip")[0], None) self.assertEqual(compressor("text/plain,*/*; charset=utf-8")[0], None) # PR #12 self.assertEqual(compressor("gzip;q==1")[0], "gzip") # questionable self.assertEqual(compressor("br;gzip")[0], "br") # questionable # self.assertEqual(compressor("br;q=0, gzip;q=0.8, *;q=0.1")[0], "gzip") self.assertEqual(compressor("*")[0], "zstd") class StreamingTest(SimpleTestCase): """ Tests streaming. """ short_string = b"This string is too short to be worth compressing." compressible_string = b"a" * 500 incompressible_string = b"".join( int2byte(random.randint(0, 255)) for _ in range(500) ) sequence = [b"a" * 500, b"b" * 200, b"a" * 300] sequence_unicode = [u"a" * 500, u"é" * 200, u"a" * 300] request_factory = RequestFactory() def setUp(self): self.req = self.request_factory.get("/") self.req.META["HTTP_ACCEPT_ENCODING"] = "gzip, deflate, br" self.req.META[ "HTTP_USER_AGENT" ] = "Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1" self.resp = HttpResponse() self.resp.status_code = 200 self.resp.content = self.compressible_string self.resp["Content-Type"] = "text/html; charset=UTF-8" self.stream_resp = StreamingHttpResponse(self.sequence) self.stream_resp["Content-Type"] = "text/html; charset=UTF-8" self.stream_resp_unicode = StreamingHttpResponse(self.sequence_unicode) self.stream_resp_unicode["Content-Type"] = "text/html; charset=UTF-8" def test_compress_streaming_response(self): """ Compression is performed on responses with streaming content. """ r = CompressionMiddleware(lambda: self.stream_resp).process_response(self.req, self.stream_resp) self.assertEqual(brotli.decompress(b"".join(r)), b"".join(self.sequence)) self.assertEqual(r.get("Content-Encoding"), "br") self.assertFalse(r.has_header("Content-Length")) self.assertEqual(r.get("Vary"), "Accept-Encoding") def test_compress_streaming_response_unicode(self): """ Compression is performed on responses with streaming Unicode content. """ r = CompressionMiddleware(lambda: self.stream_resp_unicode).process_response( self.req, self.stream_resp_unicode ) self.assertEqual( brotli.decompress(b"".join(r)), b"".join(x.encode("utf-8") for x in self.sequence_unicode) ) self.assertEqual(r.get("Content-Encoding"), "br") self.assertFalse(r.has_header("Content-Length")) self.assertEqual(r.get("Vary"), "Accept-Encoding") django-compression-middleware-0.5.0/tests/utils.py000066400000000000000000000113051440712106500223040ustar00rootroot00000000000000# -*- encoding: utf-8 -*- from __future__ import unicode_literals # Dummy text (aka Lorem ipsum) in Czech (credits: http://cs.blabot.net/) for testing compression UTF8_LOREM_IPSUM_IN_CZECH = """S úsilí kdepak využívat současníků test pivo ovcím šimpanze. silnějšímu, tj. a nezadal odeženou hlavu bránil do neúspěšné, silnějšímu, tj. zní ke svítí proběhl lo podmínkách ústní i nepravděpodobné chytré. Výskyt laně postupu nežli aktivity kousek výbavy prostředí pepřem, čím já jeden, 195. Patří tam moři propracovanoua nahrubo míst orgánu pohroma svého epidemií stehny hlídá lidem geny o mnohdy, tea chnologie já vynesl krása i zástupcům opačně letišti stavba. O provoz další opra covaných vážilo z chemical staly, tam víkendu z plyne. Snad nádherným, stranu je zdí pomezí pohřbeného epidemií mi u 540 ve volně kyčle nový mezi budov přirozené a přáteli polarizovaný parazitů svahy. Vážit je den mamutí americký slovníky nad plánoval důkaz, sloučení význam v požírají, ulice ale za všech blíž minulosti. o U domů polonica kanále – čem já zahrada silou člověka EU touto začaly chodily mr azy? Podepsala vláken postupně ve podél procházejí výjimky minimum, u naproti po vinné Moravy vermontu domněnku odhadů střediskem k činu, můj avšak ty lokalizova nému pravdou 570 něm zažijete podrobili odvětví nešlo. Dlouhou šimpanz vítejte g enerací se brázdit doplňuje podobná té uměle dobrá. Tříkilometrové kategorií, by 480 a pokroku potom k čekala lokální. Kanadského, nálezů, jestli, něm poloostroa vě, v Platón čeští duhový, nálezů z projev.\n\nObčany skotu kterých s ho stopami pravdu ze desetiletí centimetrů jeví šest a místních důležitou, plná slonice nik dy já spořádaně stavu naplaveninách prostředky profesorka. 1591 odstřihne, nebyl y tento erupce odpočinout osluněné 1 v něm je temna popis malá sága týmem astero idu z poznala standardních obeplutí názvy. Nejenže ke je hermeticky fyzici, dese t kam migrační křídla úhrnem. Již ta učí šimpanzů stádech ať boky i přispívá int ernetová k 500 ně se polohu teorii tady. Osmi po kampaň analyzovány jiná z alarm ující další tlupa.\n\nKaždý 1921 připlouváte čemž pevnosti, hornina Moravy násle y špatných, dnů ta. U ohňové v respirátorem spíš od závěru, má lidem 2005 již sv aly, běžně matky i větry výška. Demenci globálního vyvraždila jí na překvapení s lovácku bojem softwarových ty i strany půdy útěk svým s musí, 360° a vypráví šan ci s narušení. Prací nepřešlapuje 3000 krásy cihlová, víkendu s a tu z zemském c ítit větší. Vlna a uvádí kterou fatální divný společné – války zdravotním dané m ířil. O nejpalčivější skoro rodilí o tato nemohou EU zvířat připomínalo činná pl ná u rozbuška nechci tkví rozhovor roku široký. Toho dolní dopluli, či otevírá b uňky mj. viry životní středověké klimatizační zachytit chřipkou.\n\nProvází změn dravý ta svezení než takového vykreslují zemím níž ideálním pasivitou – plné mír u neon strany membránou a jel, té stromů kameni ve bílý dobrovolníků v naději mě ní o kuliček ta draci skoro ideální. Podobu vlny sérií tohle agrese restauraci z a sněžilo dávných činila, nebyl ostrova s ředitelka ředitelka, nepřestaneme, pen zionovaného k budoucnostzačne některé. Až horským zásad mé prokletých. Nobel dět i zákonů emise. Klidné příčinou tradic plně vyvodíme doplňují a nejméně specific kého tvrdí. Jí smrt při umělé objevováním.""" # type: str django-compression-middleware-0.5.0/tox.ini000066400000000000000000000022731440712106500207470ustar00rootroot00000000000000[tox] envlist = {py27}-django111-{brotlipy,Brotli} {pypy}-django111-{brotlipy,Brotli} py{34,35,36,37,py35,py36,py37}-django{111,20}-{brotlipy,Brotli} py{35,36,37,py35,py36,py37}-django{21,22}-{brotlipy,Brotli} py{38,py38}-django22-{brotlipy,Brotli} py{36,37,38,39,py36,py37,py38,py39}-django{30,31,32}-{brotlipy,Brotli} py310-django32-{brotlipy,Brotli} py{38,39,310,py38,py39}-django{40,41}-{brotlipy,Brotli} py311-django41-{brotlipy,Brotli} skip_missing_interpreters = true [testenv] setenv = PYTHONPATH = {toxinidir} commands = pytest tests/ deps = django111: Django>=1.11, <2.0 django20: Django>=2.0, < 2.1 django21: Django>=2.1, < 2.2 django22: Django>=2.2, < 3.0 django30: Django>=3.0, < 3.1 django31: Django>=3.1, < 3.2 django32: Django>=3.2, < 4.0 django40: Django>=4.0, < 4.1 django41: Django>=4.1, < 4.2 pytest>=3.1 zstandard brotlipy: brotlipy Brotli: Brotli [gh-actions] python = 2.7: py27 3.5: py35 3.6: py36 3.7: py37 3.8: py38 3.9: py39 3.10: py310 3.11: py311 pypy-2.7: pypy pypy-3.6: pypy36 pypy-3.7: pypy37 pypy-3.8: pypy38 pypy-3.9: pypy39