Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-Flask-Compress for openSUSE:Factory checked in at 2021-06-01 10:40:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Flask-Compress (Old) and /work/SRC/openSUSE:Factory/.python-Flask-Compress.new.1898 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Flask-Compress" Tue Jun 1 10:40:10 2021 rev:5 rq:896580 version:1.8.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Flask-Compress/python-Flask-Compress.changes 2020-05-16 22:27:24.733405861 +0200 +++ /work/SRC/openSUSE:Factory/.python-Flask-Compress.new.1898/python-Flask-Compress.changes 2021-06-01 10:41:45.081251235 +0200 @@ -1,0 +2,28 @@ +Thu Nov 26 17:45:07 UTC 2020 - Arun Persaud <a...@gmx.de> + +- specfile: + * update copyright year + +- update to version 1.8.0: + * Support ETag header as defined in RFC7232 #17 + * Implement per-view compression #14 + +- changes from version 1.7.0 : + * The following parameters to control Brotli compression are now + available: #10 + + COMPRESS_BR_MODE + + COMPRESS_BR_LEVEL + + COMPRESS_BR_WINDOW + + COMPRESS_BR_BLOCK + * Add deflate support, with COMPRESS_DEFLATE_LEVEL to control + compression level (default is -1) #8 + * The default quality level for Brotli is now 4, which provides + compression comparable to gzip at the default setting, while + reducing the time required versus the Brotli default of 11 + +- changes from version 1.6.0: + * Support for multiple compression algorithms and quality factors #7 + * Modified default compression settings to use Brotli when available + before gzip + +------------------------------------------------------------------- Old: ---- Flask-Compress-1.5.0.tar.gz New: ---- Flask-Compress-1.8.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Flask-Compress.spec ++++++ --- /var/tmp/diff_new_pack.bG9kwZ/_old 2021-06-01 10:41:45.513251970 +0200 +++ /var/tmp/diff_new_pack.bG9kwZ/_new 2021-06-01 10:41:45.517251978 +0200 @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-Flask-Compress -Version: 1.5.0 +Version: 1.8.0 Release: 0 Summary: Compress responses in Flask apps with gzip License: MIT ++++++ Flask-Compress-1.5.0.tar.gz -> Flask-Compress-1.8.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/Flask_Compress.egg-info/PKG-INFO new/Flask-Compress-1.8.0/Flask_Compress.egg-info/PKG-INFO --- old/Flask-Compress-1.5.0/Flask_Compress.egg-info/PKG-INFO 2020-04-27 09:10:49.000000000 +0200 +++ new/Flask-Compress-1.8.0/Flask_Compress.egg-info/PKG-INFO 2020-11-03 14:25:41.000000000 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 2.1 Name: Flask-Compress -Version: 1.5.0 -Summary: Compress responses in your Flask app with gzip or brotli. +Version: 1.8.0 +Summary: Compress responses in your Flask app with gzip, deflate or brotli. Home-page: https://github.com/colour-science/flask-compress Author: Thomas Mansencal Author-email: thomas.mansen...@gmail.com @@ -13,16 +13,25 @@ [](https://coveralls.io/github/libwilliam/flask-compress) [](https://github.com/libwilliam/flask-compress/blob/master/LICENSE.txt) - Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip. + Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip, deflate or brotli. The preferred solution is to have a server (like [Nginx](http://wiki.nginx.org/Main)) automatically compress the static files for you. If you don't have that option Flask-Compress will solve the problem for you. ## How it works - Flask-Compress both adds the various headers required for a compressed response and gzips the response data. This makes serving gzip compressed static files extremely easy. + Flask-Compress both adds the various headers required for a compressed response and compresses the response data. + This makes serving compressed static files extremely easy. - Internally, every time a request is made the extension will check if it matches one of the compressible MIME types and will automatically attach the appropriate headers. + Internally, every time a request is made the extension will check if it matches one of the compressible MIME types + and whether the client and the server use some common compression algorithm, and will automatically attach the + appropriate headers. + + To determine the compression algorithm, the `Accept-Encoding` request header is inspected, respecting the + quality factor as described in [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). + If no requested compression algorithm is supported by the server, we don't compress the response. If, on the other + hand, multiple suitable algorithms are found and are requested with the same quality factor, we choose the first one + defined in the `COMPRESS_ALGORITHM` option (see below). ## Installation @@ -48,7 +57,9 @@ ## Using Flask-Compress - Flask-Compress is incredibly simple to use. In order to start gzip'ing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. + ### Globally + + Flask-Compress is incredibly simple to use. In order to start compressing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. ```python from flask import Flask @@ -72,8 +83,27 @@ return app ``` - In terms of automatically compressing your assets using gzip, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. + In terms of automatically compressing your assets, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. + + ### Per-view compression + + Compression is possible per view using the `@compress.compressed()` decorator. Make sure to disable global compression first. + ```python + from flask import Flask + from flask_compress import Compress + + app = Flask(__name__) + app.config["COMPRESS_REGISTER"] = False # disable default compression of all eligible requests + compress = Compress() + compress.init_app(app) + + # Compress this view specifically + @app.route("/test") + @compress.compressed() + def view(): + pass + ``` ## Options @@ -83,11 +113,16 @@ | ------ | ----------- | ------- | | `COMPRESS_MIMETYPES` | Set the list of mimetypes to compress here. | `[`<br>`'text/html',`<br>`'text/css',`<br>`'text/xml',`<br>`'application/json',`<br>`'application/javascript'`<br>`]` | | `COMPRESS_LEVEL` | Specifies the gzip compression level. | `6` | + | `COMPRESS_BR_LEVEL` | Specifies the Brotli compression level. Ranges from 0 to 11. | `4` | + | `COMPRESS_BR_MODE` | For Brotli, the compression mode. The options are 0, 1, or 2. These correspond to "generic", "text" (for UTF-8 input), and "font" (for WOFF 2.0). | `0` | + | `COMPRESS_BR_WINDOW` | For Brotli, this specifies the base-2 logarithm of the sliding window size. Ranges from 10 to 24. | `22` | + | `COMPRESS_BR_BLOCK` | For Brotli, this provides the base-2 logarithm of the maximum input block size. If zero is provided, value will be determined based on the quality. Ranges from 16 to 24. | `0` | + | `COMPRESS_DEFLATE_LEVEL` | Specifies the deflate compression level. | `-1` | | `COMPRESS_MIN_SIZE` | Specifies the minimum file size threshold for compressing files. | `500` | | `COMPRESS_CACHE_KEY` | Specifies the cache key method for lookup/storage of response data. | `None` | | `COMPRESS_CACHE_BACKEND` | Specified the backend for storing the cached response data. | `None` | | `COMPRESS_REGISTER` | Specifies if compression should be automatically registered. | `True` | - | `COMPRESS_ALGORITHM` | Compression algorithm used: `gzip` or `br`. | `gzip` | + | `COMPRESS_ALGORITHM` | Supported compression algorithms. | `['br', 'gzip', 'deflate']` | Platform: any Classifier: Environment :: Web Environment diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/PKG-INFO new/Flask-Compress-1.8.0/PKG-INFO --- old/Flask-Compress-1.5.0/PKG-INFO 2020-04-27 09:10:49.000000000 +0200 +++ new/Flask-Compress-1.8.0/PKG-INFO 2020-11-03 14:25:41.266460700 +0100 @@ -1,7 +1,7 @@ Metadata-Version: 2.1 Name: Flask-Compress -Version: 1.5.0 -Summary: Compress responses in your Flask app with gzip or brotli. +Version: 1.8.0 +Summary: Compress responses in your Flask app with gzip, deflate or brotli. Home-page: https://github.com/colour-science/flask-compress Author: Thomas Mansencal Author-email: thomas.mansen...@gmail.com @@ -13,16 +13,25 @@ [](https://coveralls.io/github/libwilliam/flask-compress) [](https://github.com/libwilliam/flask-compress/blob/master/LICENSE.txt) - Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip. + Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip, deflate or brotli. The preferred solution is to have a server (like [Nginx](http://wiki.nginx.org/Main)) automatically compress the static files for you. If you don't have that option Flask-Compress will solve the problem for you. ## How it works - Flask-Compress both adds the various headers required for a compressed response and gzips the response data. This makes serving gzip compressed static files extremely easy. + Flask-Compress both adds the various headers required for a compressed response and compresses the response data. + This makes serving compressed static files extremely easy. - Internally, every time a request is made the extension will check if it matches one of the compressible MIME types and will automatically attach the appropriate headers. + Internally, every time a request is made the extension will check if it matches one of the compressible MIME types + and whether the client and the server use some common compression algorithm, and will automatically attach the + appropriate headers. + + To determine the compression algorithm, the `Accept-Encoding` request header is inspected, respecting the + quality factor as described in [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). + If no requested compression algorithm is supported by the server, we don't compress the response. If, on the other + hand, multiple suitable algorithms are found and are requested with the same quality factor, we choose the first one + defined in the `COMPRESS_ALGORITHM` option (see below). ## Installation @@ -48,7 +57,9 @@ ## Using Flask-Compress - Flask-Compress is incredibly simple to use. In order to start gzip'ing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. + ### Globally + + Flask-Compress is incredibly simple to use. In order to start compressing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. ```python from flask import Flask @@ -72,8 +83,27 @@ return app ``` - In terms of automatically compressing your assets using gzip, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. + In terms of automatically compressing your assets, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. + + ### Per-view compression + + Compression is possible per view using the `@compress.compressed()` decorator. Make sure to disable global compression first. + ```python + from flask import Flask + from flask_compress import Compress + + app = Flask(__name__) + app.config["COMPRESS_REGISTER"] = False # disable default compression of all eligible requests + compress = Compress() + compress.init_app(app) + + # Compress this view specifically + @app.route("/test") + @compress.compressed() + def view(): + pass + ``` ## Options @@ -83,11 +113,16 @@ | ------ | ----------- | ------- | | `COMPRESS_MIMETYPES` | Set the list of mimetypes to compress here. | `[`<br>`'text/html',`<br>`'text/css',`<br>`'text/xml',`<br>`'application/json',`<br>`'application/javascript'`<br>`]` | | `COMPRESS_LEVEL` | Specifies the gzip compression level. | `6` | + | `COMPRESS_BR_LEVEL` | Specifies the Brotli compression level. Ranges from 0 to 11. | `4` | + | `COMPRESS_BR_MODE` | For Brotli, the compression mode. The options are 0, 1, or 2. These correspond to "generic", "text" (for UTF-8 input), and "font" (for WOFF 2.0). | `0` | + | `COMPRESS_BR_WINDOW` | For Brotli, this specifies the base-2 logarithm of the sliding window size. Ranges from 10 to 24. | `22` | + | `COMPRESS_BR_BLOCK` | For Brotli, this provides the base-2 logarithm of the maximum input block size. If zero is provided, value will be determined based on the quality. Ranges from 16 to 24. | `0` | + | `COMPRESS_DEFLATE_LEVEL` | Specifies the deflate compression level. | `-1` | | `COMPRESS_MIN_SIZE` | Specifies the minimum file size threshold for compressing files. | `500` | | `COMPRESS_CACHE_KEY` | Specifies the cache key method for lookup/storage of response data. | `None` | | `COMPRESS_CACHE_BACKEND` | Specified the backend for storing the cached response data. | `None` | | `COMPRESS_REGISTER` | Specifies if compression should be automatically registered. | `True` | - | `COMPRESS_ALGORITHM` | Compression algorithm used: `gzip` or `br`. | `gzip` | + | `COMPRESS_ALGORITHM` | Supported compression algorithms. | `['br', 'gzip', 'deflate']` | Platform: any Classifier: Environment :: Web Environment diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/README.md new/Flask-Compress-1.8.0/README.md --- old/Flask-Compress-1.5.0/README.md 2020-04-21 10:36:48.000000000 +0200 +++ new/Flask-Compress-1.8.0/README.md 2020-11-02 09:38:17.000000000 +0100 @@ -5,16 +5,25 @@ [](https://coveralls.io/github/libwilliam/flask-compress) [](https://github.com/libwilliam/flask-compress/blob/master/LICENSE.txt) -Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip. +Flask-Compress allows you to easily compress your [Flask](http://flask.pocoo.org/) application's responses with gzip, deflate or brotli. The preferred solution is to have a server (like [Nginx](http://wiki.nginx.org/Main)) automatically compress the static files for you. If you don't have that option Flask-Compress will solve the problem for you. ## How it works -Flask-Compress both adds the various headers required for a compressed response and gzips the response data. This makes serving gzip compressed static files extremely easy. +Flask-Compress both adds the various headers required for a compressed response and compresses the response data. +This makes serving compressed static files extremely easy. -Internally, every time a request is made the extension will check if it matches one of the compressible MIME types and will automatically attach the appropriate headers. +Internally, every time a request is made the extension will check if it matches one of the compressible MIME types +and whether the client and the server use some common compression algorithm, and will automatically attach the +appropriate headers. + +To determine the compression algorithm, the `Accept-Encoding` request header is inspected, respecting the +quality factor as described in [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding). +If no requested compression algorithm is supported by the server, we don't compress the response. If, on the other +hand, multiple suitable algorithms are found and are requested with the same quality factor, we choose the first one +defined in the `COMPRESS_ALGORITHM` option (see below). ## Installation @@ -40,7 +49,9 @@ ## Using Flask-Compress -Flask-Compress is incredibly simple to use. In order to start gzip'ing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. +### Globally + +Flask-Compress is incredibly simple to use. In order to start compressing your Flask application's assets, the first thing to do is let Flask-Compress know about your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) application object. ```python from flask import Flask @@ -64,8 +75,27 @@ return app ``` -In terms of automatically compressing your assets using gzip, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. +In terms of automatically compressing your assets, passing your [`flask.Flask`](http://flask.pocoo.org/docs/latest/api/#flask.Flask) object to the `flask_compress.Compress` object is all that needs to be done. + +### Per-view compression + +Compression is possible per view using the `@compress.compressed()` decorator. Make sure to disable global compression first. +```python +from flask import Flask +from flask_compress import Compress + +app = Flask(__name__) +app.config["COMPRESS_REGISTER"] = False # disable default compression of all eligible requests +compress = Compress() +compress.init_app(app) + +# Compress this view specifically +@app.route("/test") +@compress.compressed() +def view(): + pass +``` ## Options @@ -75,8 +105,13 @@ | ------ | ----------- | ------- | | `COMPRESS_MIMETYPES` | Set the list of mimetypes to compress here. | `[`<br>`'text/html',`<br>`'text/css',`<br>`'text/xml',`<br>`'application/json',`<br>`'application/javascript'`<br>`]` | | `COMPRESS_LEVEL` | Specifies the gzip compression level. | `6` | +| `COMPRESS_BR_LEVEL` | Specifies the Brotli compression level. Ranges from 0 to 11. | `4` | +| `COMPRESS_BR_MODE` | For Brotli, the compression mode. The options are 0, 1, or 2. These correspond to "generic", "text" (for UTF-8 input), and "font" (for WOFF 2.0). | `0` | +| `COMPRESS_BR_WINDOW` | For Brotli, this specifies the base-2 logarithm of the sliding window size. Ranges from 10 to 24. | `22` | +| `COMPRESS_BR_BLOCK` | For Brotli, this provides the base-2 logarithm of the maximum input block size. If zero is provided, value will be determined based on the quality. Ranges from 16 to 24. | `0` | +| `COMPRESS_DEFLATE_LEVEL` | Specifies the deflate compression level. | `-1` | | `COMPRESS_MIN_SIZE` | Specifies the minimum file size threshold for compressing files. | `500` | | `COMPRESS_CACHE_KEY` | Specifies the cache key method for lookup/storage of response data. | `None` | | `COMPRESS_CACHE_BACKEND` | Specified the backend for storing the cached response data. | `None` | | `COMPRESS_REGISTER` | Specifies if compression should be automatically registered. | `True` | -| `COMPRESS_ALGORITHM` | Compression algorithm used: `gzip` or `br`. | `gzip` | +| `COMPRESS_ALGORITHM` | Supported compression algorithms. | `['br', 'gzip', 'deflate']` | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/flask_compress.py new/Flask-Compress-1.8.0/flask_compress.py --- old/Flask-Compress-1.5.0/flask_compress.py 2020-04-21 09:38:27.000000000 +0200 +++ new/Flask-Compress-1.8.0/flask_compress.py 2020-11-03 09:34:40.000000000 +0100 @@ -4,11 +4,15 @@ # License: The MIT License (MIT) import sys +import functools from gzip import GzipFile +import zlib from io import BytesIO +from collections import defaultdict + import brotli -from flask import request, current_app +from flask import request, after_this_request, current_app if sys.version_info[:2] == (2, 6): @@ -64,11 +68,16 @@ 'application/json', 'application/javascript']), ('COMPRESS_LEVEL', 6), + ('COMPRESS_BR_LEVEL', 4), + ('COMPRESS_BR_MODE', 0), + ('COMPRESS_BR_WINDOW', 22), + ('COMPRESS_BR_BLOCK', 0), + ('COMPRESS_DEFLATE_LEVEL', -1), ('COMPRESS_MIN_SIZE', 500), ('COMPRESS_CACHE_KEY', None), ('COMPRESS_CACHE_BACKEND', None), ('COMPRESS_REGISTER', True), - ('COMPRESS_ALGORITHM', 'gzip'), + ('COMPRESS_ALGORITHM', ['br', 'gzip', 'deflate']), ] for k, v in defaults: @@ -78,55 +87,147 @@ self.cache = backend() if backend else None self.cache_key = app.config['COMPRESS_CACHE_KEY'] + algo = app.config['COMPRESS_ALGORITHM'] + if isinstance(algo, str): + self.enabled_algorithms = [i.strip() for i in algo.split(',')] + else: + self.enabled_algorithms = algo + if (app.config['COMPRESS_REGISTER'] and app.config['COMPRESS_MIMETYPES']): app.after_request(self.after_request) + def _choose_compress_algorithm(self, accept_encoding_header): + """ + Determine which compression algorithm we're going to use based on the + client request. The `Accept-Encoding` header may list one or more desired + algorithms, together with a "quality factor" for each one (higher quality + means the client prefers that algorithm more). + + :param accept_encoding_header: Content of the `Accept-Encoding` header + :return: name of a compression algorithm (`gzip`, `deflate`, `br`) or `None` if + the client and server don't agree on any. + """ + # Map quality factors to requested algorithm names. + algos_by_quality = defaultdict(set) + + # A flag denoting that client requested using any (`*`) algorithm, + # in case a specific one is not supported by the server + fallback_to_any = False + + for part in accept_encoding_header.lower().split(','): + part = part.strip() + quality = 1.0 + + if ';q=' in part: + # If the client associated a quality factor with an algorithm, + # try to parse it. We could do the matching using a regex, but + # the format is so simple that it would be overkill. + algo = part.split(';')[0].strip() + try: + quality = float(part.split('=')[1].strip()) + except ValueError: + pass + else: + # Otherwise, use the default quality + algo = part + + algos_by_quality[quality].add(algo) + fallback_to_any = fallback_to_any or (algo == '*') + + # Choose the algorithm with the highest quality factor that the server supports. + # + # If there are multiple equally good options, choose the first supported algorithm + # from server configuration. + # + # If the server doesn't support any algorithm that the client requested but + # there's a special wildcard algorithm request (`*`), choose the first supported + # algorithm. + server_algo_set = set(self.enabled_algorithms) + for _, requested_algo_set in sorted(algos_by_quality.items(), reverse=True): + viable_algos = server_algo_set & requested_algo_set + if len(viable_algos) == 1: + return viable_algos.pop() + elif len(viable_algos) > 1: + for server_algo in self.enabled_algorithms: + if server_algo in viable_algos: + return server_algo + else: + if fallback_to_any: + return self.enabled_algorithms[0] + + return None + def after_request(self, response): app = self.app or current_app + accept_encoding = request.headers.get('Accept-Encoding', '') + chosen_algorithm = self._choose_compress_algorithm(accept_encoding) - if (response.mimetype not in app.config['COMPRESS_MIMETYPES'] or - ('gzip' not in accept_encoding.lower() and app.config['COMPRESS_ALGORITHM'] == 'gzip') or - ('br' not in accept_encoding.lower() and app.config['COMPRESS_ALGORITHM'] == 'br') or - not 200 <= response.status_code < 300 or + if (chosen_algorithm is None or + response.mimetype not in app.config["COMPRESS_MIMETYPES"] or + response.status_code < 200 or + response.status_code >= 300 or + "Content-Encoding" in response.headers or (response.content_length is not None and - response.content_length < app.config['COMPRESS_MIN_SIZE']) or - 'Content-Encoding' in response.headers): + response.content_length < app.config["COMPRESS_MIN_SIZE"])): return response response.direct_passthrough = False - if self.cache: + if self.cache is not None: key = self.cache_key(request) compressed_content = self.cache.get(key) if compressed_content is None: - compressed_content = self.compress(app, response) + compressed_content = self.compress(app, response, chosen_algorithm) self.cache.set(key, compressed_content) else: - compressed_content = self.compress(app, response) + compressed_content = self.compress(app, response, chosen_algorithm) response.set_data(compressed_content) - response.headers['Content-Encoding'] = app.config['COMPRESS_ALGORITHM'] + response.headers['Content-Encoding'] = chosen_algorithm response.headers['Content-Length'] = response.content_length + # "123456789" => "123456789:gzip" - A strong ETag validator + # W/"123456789" => W/"123456789:gzip" - A weak ETag validator + etag = response.headers.get('ETag') + if etag: + response.headers['ETag'] = '{0}:{1}"'.format(etag[:-1], chosen_algorithm) + vary = response.headers.get('Vary') - if vary: - if 'accept-encoding' not in vary.lower(): - response.headers['Vary'] = '{}, Accept-Encoding'.format(vary) - else: + if not vary: response.headers['Vary'] = 'Accept-Encoding' + elif 'accept-encoding' not in vary.lower(): + response.headers['Vary'] = '{}, Accept-Encoding'.format(vary) return response - def compress(self, app, response): - if app.config['COMPRESS_ALGORITHM'] == 'gzip': + def compressed(self): + def decorator(f): + @functools.wraps(f) + def decorated_function(*args, **kwargs): + @after_this_request + def compressor(response): + return self.after_request(response) + return f(*args, **kwargs) + return decorated_function + return decorator + + def compress(self, app, response, algorithm): + if algorithm == 'gzip': gzip_buffer = BytesIO() with GzipFile(mode='wb', compresslevel=app.config['COMPRESS_LEVEL'], fileobj=gzip_buffer) as gzip_file: gzip_file.write(response.get_data()) return gzip_buffer.getvalue() - elif app.config['COMPRESS_ALGORITHM'] == 'br': - return brotli.compress(response.get_data()) + elif algorithm == 'deflate': + return zlib.compress(response.get_data(), + app.config['COMPRESS_DEFLATE_LEVEL']) + elif algorithm == 'br': + return brotli.compress(response.get_data(), + mode=app.config['COMPRESS_BR_MODE'], + quality=app.config['COMPRESS_BR_LEVEL'], + lgwin=app.config['COMPRESS_BR_WINDOW'], + lgblock=app.config['COMPRESS_BR_BLOCK']) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/setup.py new/Flask-Compress-1.8.0/setup.py --- old/Flask-Compress-1.5.0/setup.py 2020-04-27 09:06:00.000000000 +0200 +++ new/Flask-Compress-1.8.0/setup.py 2020-11-03 14:20:11.000000000 +0100 @@ -5,12 +5,12 @@ setuptools.setup( name='Flask-Compress', - version='1.5.0', + version='1.8.0', url='https://github.com/colour-science/flask-compress', license='MIT', author='Thomas Mansencal', author_email='thomas.mansen...@gmail.com', - description='Compress responses in your Flask app with gzip or brotli.', + description='Compress responses in your Flask app with gzip, deflate or brotli.', long_description=LONG_DESCRIPTION, long_description_content_type='text/markdown', py_modules=['flask_compress'], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Flask-Compress-1.5.0/tests/test_flask_compress.py new/Flask-Compress-1.8.0/tests/test_flask_compress.py --- old/Flask-Compress-1.5.0/tests/test_flask_compress.py 2020-04-21 09:38:27.000000000 +0200 +++ new/Flask-Compress-1.8.0/tests/test_flask_compress.py 2020-11-02 09:38:17.000000000 +0100 @@ -29,8 +29,27 @@ def test_algorithm_default(self): """ Tests COMPRESS_ALGORITHM default value is correctly set. """ - self.assertEqual(self.app.config['COMPRESS_ALGORITHM'], 'gzip') + self.assertEqual(self.app.config['COMPRESS_ALGORITHM'], ['br', 'gzip', 'deflate']) + def test_default_deflate_settings(self): + """ Tests COMPRESS_DELATE_LEVEL default value is correctly set. """ + self.assertEqual(self.app.config['COMPRESS_DEFLATE_LEVEL'], -1) + + def test_mode_default(self): + """ Tests COMPRESS_BR_MODE default value is correctly set. """ + self.assertEqual(self.app.config['COMPRESS_BR_MODE'], 0) + + def test_quality_level_default(self): + """ Tests COMPRESS_BR_LEVEL default value is correctly set. """ + self.assertEqual(self.app.config['COMPRESS_BR_LEVEL'], 4) + + def test_window_size_default(self): + """ Tests COMPRESS_BR_WINDOW default value is correctly set. """ + self.assertEqual(self.app.config['COMPRESS_BR_WINDOW'], 22) + + def test_block_size_default(self): + """ Tests COMPRESS_BR_BLOCK default value is correctly set. """ + self.assertEqual(self.app.config['COMPRESS_BR_BLOCK'], 0) class InitTests(unittest.TestCase): def setUp(self): @@ -85,18 +104,6 @@ response = client.options('/large/', headers=headers) self.assertEqual(response.status_code, 200) - def test_compress_level(self): - """ Tests COMPRESS_LEVEL correctly affects response data. """ - self.app.config['COMPRESS_LEVEL'] = 1 - response = self.client_get('/large/') - response1_size = len(response.data) - - self.app.config['COMPRESS_LEVEL'] = 6 - response = self.client_get('/large/') - response6_size = len(response.data) - - self.assertNotEqual(response1_size, response6_size) - def test_compress_min_size(self): """ Tests COMPRESS_MIN_SIZE correctly affects response data. """ response = self.client_get('/small/') @@ -116,5 +123,210 @@ response = client.options('/small/', headers=headers) self.assertEqual(response.status_code, 200) + def test_gzip_compression_level(self): + """ Tests COMPRESS_LEVEL correctly affects response data. """ + self.app.config['COMPRESS_LEVEL'] = 1 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'gzip')]) + response1_size = len(response.data) + + self.app.config['COMPRESS_LEVEL'] = 6 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'gzip')]) + response6_size = len(response.data) + + self.assertNotEqual(response1_size, response6_size) + + def test_br_compression_level(self): + """ Tests that COMPRESS_BR_LEVEL correctly affects response data. """ + self.app.config['COMPRESS_BR_LEVEL'] = 4 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'br')]) + response4_size = len(response.data) + + self.app.config['COMPRESS_BR_LEVEL'] = 11 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'br')]) + response11_size = len(response.data) + + self.assertNotEqual(response4_size, response11_size) + + def test_deflate_compression_level(self): + """ Tests COMPRESS_DELATE_LEVEL correctly affects response data. """ + self.app.config['COMPRESS_DEFLATE_LEVEL'] = -1 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'deflate')]) + response_size = len(response.data) + + self.app.config['COMPRESS_DEFLATE_LEVEL'] = 1 + client = self.app.test_client() + response = client.get('/large/', headers=[('Accept-Encoding', 'deflate')]) + response1_size = len(response.data) + + self.assertNotEqual(response_size, response1_size) + + +class CompressionAlgoTests(unittest.TestCase): + """ + Test different scenarios for compression algorithm negotiation between + client and server. Please note that algorithm names (even the "supported" + ones) in these tests **do not** indicate that all of these are actually + supported by this extension. + """ + def setUp(self): + super(CompressionAlgoTests, self).setUp() + + # Create the app here but don't call `Compress()` on it just yet; we need + # to be able to modify the settings in various tests. Calling `Compress(self.app)` + # twice would result in two `@after_request` handlers, which would be bad. + self.app = Flask(__name__) + self.app.testing = True + + small_path = os.path.join(os.getcwd(), 'tests', 'templates', 'small.html') + self.small_size = os.path.getsize(small_path) - 1 + + @self.app.route('/small/') + def small(): + return render_template('small.html') + + def test_setting_compress_algorithm_simple_string(self): + """ Test that a single entry in `COMPRESS_ALGORITHM` still works for backwards compatibility """ + self.app.config['COMPRESS_ALGORITHM'] = 'gzip' + c = Compress(self.app) + self.assertListEqual(c.enabled_algorithms, ['gzip']) + + def test_setting_compress_algorithm_cs_string(self): + """ Test that `COMPRESS_ALGORITHM` can be a comma-separated string """ + self.app.config['COMPRESS_ALGORITHM'] = 'gzip, br, zstd' + c = Compress(self.app) + self.assertListEqual(c.enabled_algorithms, ['gzip', 'br', 'zstd']) + + def test_setting_compress_algorithm_list(self): + """ Test that `COMPRESS_ALGORITHM` can be a list of strings """ + self.app.config['COMPRESS_ALGORITHM'] = ['gzip', 'br', 'deflate'] + c = Compress(self.app) + self.assertListEqual(c.enabled_algorithms, ['gzip', 'br', 'deflate']) + + def test_one_algo_supported(self): + """ Tests requesting a single supported compression algorithm """ + accept_encoding = 'gzip' + self.app.config['COMPRESS_ALGORITHM'] = ['br', 'gzip'] + c = Compress(self.app) + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'gzip') + + def test_one_algo_unsupported(self): + """ Tests requesting single unsupported compression algorithm """ + accept_encoding = 'some-alien-algorithm' + self.app.config['COMPRESS_ALGORITHM'] = ['br', 'gzip'] + c = Compress(self.app) + self.assertIsNone(c._choose_compress_algorithm(accept_encoding)) + + def test_multiple_algos_supported(self): + """ Tests requesting multiple supported compression algorithms """ + accept_encoding = 'br, gzip, zstd' + self.app.config['COMPRESS_ALGORITHM'] = ['zstd', 'br', 'gzip'] + c = Compress(self.app) + # When the decision is tied, we expect to see the first server-configured algorithm + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'zstd') + + def test_multiple_algos_unsupported(self): + """ Tests requesting multiple unsupported compression algorithms """ + accept_encoding = 'future-algo, alien-algo, forbidden-algo' + self.app.config['COMPRESS_ALGORITHM'] = ['zstd', 'br', 'gzip'] + c = Compress(self.app) + self.assertIsNone(c._choose_compress_algorithm(accept_encoding)) + + def test_multiple_algos_with_wildcard(self): + """ Tests requesting multiple unsupported compression algorithms and a wildcard """ + accept_encoding = 'future-algo, alien-algo, forbidden-algo, *' + self.app.config['COMPRESS_ALGORITHM'] = ['zstd', 'br', 'gzip'] + c = Compress(self.app) + # We expect to see the first server-configured algorithm + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'zstd') + + def test_multiple_algos_with_different_quality(self): + """ Tests requesting multiple supported compression algorithms with different q-factors """ + accept_encoding = 'zstd;q=0.8, br;q=0.9, gzip;q=0.5' + self.app.config['COMPRESS_ALGORITHM'] = ['zstd', 'br', 'gzip'] + c = Compress(self.app) + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'br') + + def test_multiple_algos_with_equal_quality(self): + """ Tests requesting multiple supported compression algorithms with equal q-factors """ + accept_encoding = 'zstd;q=0.5, br;q=0.5, gzip;q=0.5' + self.app.config['COMPRESS_ALGORITHM'] = ['gzip', 'br', 'zstd'] + c = Compress(self.app) + # We expect to see the first server-configured algorithm + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'gzip') + + def test_default_quality_is_1(self): + """ Tests that when making mixed-quality requests, the default q-factor is 1.0 """ + accept_encoding = 'deflate, br;q=0.999, gzip;q=0.5' + self.app.config['COMPRESS_ALGORITHM'] = ['gzip', 'br', 'deflate'] + c = Compress(self.app) + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'deflate') + + def test_default_wildcard_quality_is_0(self): + """ Tests that a wildcard has a default q-factor of 0.0 """ + accept_encoding = 'br;q=0.001, *' + self.app.config['COMPRESS_ALGORITHM'] = ['gzip', 'br', 'deflate'] + c = Compress(self.app) + self.assertEqual(c._choose_compress_algorithm(accept_encoding), 'br') + + def test_content_encoding_is_correct(self): + """ Test that the `Content-Encoding` header matches the compression algorithm """ + self.app.config['COMPRESS_ALGORITHM'] = ['br', 'gzip', 'deflate'] + Compress(self.app) + + headers_gzip = [('Accept-Encoding', 'gzip')] + client = self.app.test_client() + response_gzip = client.options('/small/', headers=headers_gzip) + self.assertIn('Content-Encoding', response_gzip.headers) + self.assertEqual(response_gzip.headers.get('Content-Encoding'), 'gzip') + + headers_br = [('Accept-Encoding', 'br')] + client = self.app.test_client() + response_br = client.options('/small/', headers=headers_br) + self.assertIn('Content-Encoding', response_br.headers) + self.assertEqual(response_br.headers.get('Content-Encoding'), 'br') + + headers_deflate = [('Accept-Encoding', 'deflate')] + client = self.app.test_client() + response_deflate = client.options('/small/', headers=headers_deflate) + self.assertIn('Content-Encoding', response_deflate.headers) + self.assertEqual(response_deflate.headers.get('Content-Encoding'), 'deflate') + + +class CompressionPerViewTests(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.app.testing = True + self.app.config["COMPRESS_REGISTER"] = False + compress = Compress() + compress.init_app(self.app) + + @self.app.route('/route1/') + def view_1(): + return render_template('large.html') + + @self.app.route('/route2/') + @compress.compressed() + def view_2(): + return render_template('large.html') + + def test_compression(self): + client = self.app.test_client() + headers = [('Accept-Encoding', 'deflate')] + + response = client.get('/route1/', headers=headers) + self.assertEqual(response.status_code, 200) + self.assertNotIn('Content-Encoding', response.headers) + + response = client.get('/route2/', headers=headers) + self.assertEqual(response.status_code, 200) + self.assertIn('Content-Encoding', response.headers) + self.assertEqual(response.headers.get('Content-Encoding'), 'deflate') + + if __name__ == '__main__': unittest.main()