├── .gitignore ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTORS.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── flask_optimize ├── __init__.py └── optimize.py ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── run_app.py └── templates └── load_svg.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules. 2 | .pypirc 3 | .idea/ 4 | *.pyc 5 | 6 | # Setuptools distribution folder. 7 | src/ 8 | env/ 9 | build/ 10 | dist/ 11 | 12 | # Python egg metadata, regenerated from source files by setuptools. 13 | *.egg-info 14 | *.egg -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "2.6" 7 | - "2.7" 8 | - "3.4" 9 | - "3.5" 10 | 11 | before_install: 12 | - pip install --upgrade pip --quiet 13 | 14 | install: 15 | - pip install -r requirements.txt 16 | 17 | before_script: 18 | - pyclean --verbose . 19 | 20 | script: 21 | - python flask_optimize/optimize.py 22 | 23 | notifications: 24 | email: false -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | - 2016/04/28 -- Initial project 2 | - 2016/12/20 -- Support optimize all response 3 | - 2018/06/24 -- Remove unnecessary code -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunary/flask-optimize/564f2e8823acb31ea3362f26f7766847bed77162/CONTRIBUTORS.txt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | All code is released under the MIT License: 2 | 3 | Copyright 2016 Nhat Vo Van (a.k.a Sunary) and contributors (see CONTRIBUTORS.txt) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md CHANGES.md LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flask-optimize 2 | 3 | [](https://travis-ci.org/sunary/flask-optimize) 4 | 5 | **Flask optimization using cache, minify html and compress response** 6 | 7 | https://pypi.python.org/pypi/flask-optimize 8 | 9 | ## Optimize parameters 10 | 11 | ### init_app: 12 | 13 | **config:** Global config 14 | 15 | Default: 16 | 17 | ```python 18 | { 19 | 'html': {'htmlmin': True, 'compress': True, 'cache': 'GET-84600'}, 20 | 'json': {'htmlmin': False, 'compress': True, 'cache': False}, 21 | 'text': {'htmlmin': False, 'compress': True, 'cache': 'GET-600'} 22 | } 23 | ``` 24 | 25 | `html`, `json`, `text`: keys for data type of response, see detail below 26 | 27 | **config_update:** update into default global config 28 | 29 | ### optimize 30 | 31 | **dtype:** Data type of response, will get value corresponding with key in global config. 32 | 33 | - `html` *(default)* 34 | - `text` 35 | - `json` 36 | 37 | **htmlmin:** Override `htmlmin` in config by key **dtype** 38 | 39 | ``` 40 | None: using default value from global config 41 | False: disable 42 | True: enable minify html 43 | ``` 44 | 45 | **compress:** Override `compress` in config by key **dtype** 46 | 47 | ``` 48 | None: using default value from global config 49 | False: disable 50 | True: enable compress content (using gzip) 51 | ``` 52 | 53 | **cache:** Override `cache` in config by key **dtype** 54 | 55 | ``` 56 | None: using default value from global config 57 | False: disable 58 | integer value: enable all method, value is cached period (seconds) 59 | GET-84600: enable for GET method only, cached period is 84600 seconds 60 | ``` 61 | 62 | 63 | ## Python code usage 64 | 65 | ```python 66 | from flask import Flask 67 | from flask_optimize import FlaskOptimize 68 | 69 | flask_app = Flask(__name__) 70 | flask_optimize = FlaskOptimize() 71 | 72 | @flask_app.route('/') 73 | @flask_optimize.optimize() 74 | def index(): 75 | return 'using Flask-optimize' 76 | 77 | @flask_app.route('/html') 78 | @flask_optimize.optimize() 79 | def html(): 80 | return ''' 81 | 82 |
83 | Default data type is html. 84 | This content will be minified. 85 | 86 | 87 | ''' 88 | 89 | @flask_app.route('/text') 90 | @flask_optimize.optimize('text') 91 | def text(): 92 | return ''' 93 | 94 | 95 | Data type response is text, so this content wasn't minified 96 | 97 | 98 | ''' 99 | 100 | @flask_app.route('/json') 101 | @flask_optimize.optimize('json') 102 | def json(): 103 | return {'text': 'anything', 'other_values': [1, 2, 3, 4]} 104 | 105 | if __name__ == '__main__': 106 | flask_app.run('localhost', 8080, debug=True) 107 | ``` 108 | 109 | ## Install 110 | 111 | ```shell 112 | pip install flask-optimize 113 | ``` 114 | 115 | ## Requirements: ## 116 | 117 | * Python 2.x 118 | * Python 3.x 119 | -------------------------------------------------------------------------------- /flask_optimize/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sunary' 2 | 3 | 4 | VERSION = '0.2.9.8' 5 | 6 | from .optimize import FlaskOptimize 7 | -------------------------------------------------------------------------------- /flask_optimize/optimize.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sunary' 2 | 3 | 4 | import sys 5 | import gzip 6 | import time 7 | from htmlmin.main import minify 8 | from flask import request, Response, make_response, current_app, json, wrappers 9 | from functools import update_wrapper 10 | 11 | 12 | IS_PYTHON_3 = True 13 | if sys.version_info[0] < 3: 14 | from StringIO import StringIO 15 | IS_PYTHON_3 = False 16 | else: 17 | from io import BytesIO 18 | 19 | 20 | class FlaskOptimize(object): 21 | 22 | _cache = {} 23 | _timestamp = {} 24 | 25 | def __init__(self, 26 | config=None): 27 | """ 28 | Global config for flask optimize foreach respond return type 29 | Args: 30 | config: global configure values 31 | """ 32 | if config is None: 33 | config = { 34 | 'html': {'htmlmin': True, 'compress': True, 'cache': 'GET-84600'}, 35 | 'json': {'htmlmin': False, 'compress': True, 'cache': False}, 36 | 'text': {'htmlmin': False, 'compress': True, 'cache': 'GET-84600'}, 37 | 'trim_fragment': False, 38 | } 39 | 40 | self.config = config 41 | 42 | def optimize(self, 43 | dtype='html', 44 | htmlmin=None, 45 | compress=None, 46 | cache=None): 47 | """ 48 | Flask optimize respond using minify html, zip content and mem cache. 49 | Elastic optimization and create Cross-site HTTP requests if respond is json 50 | Args: 51 | dtype: response type: 52 | - `html` (default) 53 | - `text` 54 | - `json` 55 | htmlmin: minify html 56 | None (default): using global config, 57 | False: disable minify html 58 | True: enable minify html 59 | compress: send content in compress (gzip) format 60 | None (default): using global config, 61 | False: disable compress response, 62 | True: enable compress response 63 | cache: cache content in RAM 64 | None (default): using global config, 65 | False: disable cache, 66 | integer: cache all method with period 67 | string value: 'METHOD-seconds' to select METHOD and period cache, eg: 'GET-3600', 'GET|POST-600', ... 68 | Examples: 69 | @optimize(dtype='html', htmlmin=True, compress=True, cache='GET-84600') 70 | """ 71 | 72 | def _decorating_wrapper(func): 73 | 74 | def _optimize_wrapper(*args, **kwargs): 75 | # default values: 76 | is_htmlmin = False 77 | is_compress = False 78 | period_cache = 0 79 | 80 | if self.config.get(dtype): 81 | is_htmlmin = self.config.get(dtype)['htmlmin'] if (htmlmin is None) else htmlmin 82 | is_compress = self.config.get(dtype)['compress'] if (compress is None) else compress 83 | cache_agrs = self.config.get(dtype)['cache'] if (cache is None) else cache 84 | 85 | if cache_agrs is False or cache_agrs == 0: 86 | period_cache = 0 87 | elif isinstance(cache_agrs, int): 88 | period_cache = cache_agrs 89 | elif isinstance(cache_agrs, str if IS_PYTHON_3 else (str, basestring)) and\ 90 | len(cache_agrs.split('-')) == 2: 91 | try: 92 | period_cache = int(cache_agrs.split('-')[1]) if (request.method in cache_agrs) else 0 93 | except (KeyError, ValueError): 94 | raise ValueError('Cache must be string with method and period cache split by "-"') 95 | else: 96 | raise ValueError('Cache must be False, int or string with method and period cache split by "-"') 97 | 98 | # init cached data 99 | now = time.time() 100 | key_cache = request.method + request.url 101 | if self.config.get('trim_fragment'): 102 | key_cache = key_cache.split('#')[0] 103 | 104 | if period_cache > 0 and self._timestamp.get(key_cache) and self._timestamp.get(key_cache) > now: 105 | return self._cache[key_cache] 106 | 107 | resp = func(*args, **kwargs) 108 | 109 | if not isinstance(resp, wrappers.Response): 110 | # crossdomain 111 | if dtype == 'json': 112 | resp = self.crossdomain(resp) 113 | 114 | # min html 115 | if is_htmlmin: 116 | resp = self.validate(self.minifier, resp) 117 | 118 | # compress 119 | if is_compress: 120 | resp = self.validate(self.compress, resp) 121 | 122 | # cache 123 | if period_cache > 0: 124 | self._cache[key_cache] = resp 125 | self._timestamp[key_cache] = now + period_cache 126 | 127 | return resp 128 | 129 | return update_wrapper(_optimize_wrapper, func) 130 | 131 | return _decorating_wrapper 132 | 133 | @staticmethod 134 | def validate(method, content): 135 | instances_compare = (str, Response) if IS_PYTHON_3 else (str, unicode, Response) 136 | if isinstance(content, instances_compare): 137 | return method(content) 138 | elif isinstance(content, tuple): 139 | if len(content) < 2: 140 | raise TypeError('Content must have larger than 2 elements') 141 | 142 | return method(content[0]), content[1] 143 | 144 | return content 145 | 146 | @staticmethod 147 | def minifier(content): 148 | if not IS_PYTHON_3 and isinstance(content, str): 149 | content = unicode(content, 'utf-8') 150 | 151 | return minify(content, 152 | remove_comments=True, 153 | reduce_empty_attributes=True, 154 | remove_optional_attribute_quotes=False) 155 | 156 | @staticmethod 157 | def compress(content): 158 | """ 159 | Compress str, unicode content using gzip 160 | """ 161 | resp = Response() 162 | if isinstance(content, Response): 163 | resp = content 164 | content = resp.data 165 | 166 | if not IS_PYTHON_3 and isinstance(content, unicode): 167 | content = content.encode('utf8') 168 | 169 | if IS_PYTHON_3: 170 | gzip_buffer = BytesIO() 171 | gzip_file = gzip.GzipFile(fileobj=gzip_buffer, mode='wb') 172 | gzip_file.write(bytes(content, 'utf-8')) 173 | else: 174 | gzip_buffer = StringIO() 175 | gzip_file = gzip.GzipFile(fileobj=gzip_buffer, mode='wb') 176 | gzip_file.write(content) 177 | 178 | gzip_file.close() 179 | 180 | resp.data = gzip_buffer.getvalue() 181 | resp.headers['Content-Encoding'] = 'gzip' 182 | resp.headers['Vary'] = 'Accept-Encoding' 183 | resp.headers['Content-Length'] = len(resp.data) 184 | 185 | return resp 186 | 187 | @staticmethod 188 | def crossdomain(content): 189 | """ 190 | Create decorator Cross-site HTTP requests 191 | see more at: http://flask.pocoo.org/snippets/56/ 192 | """ 193 | if isinstance(content, (dict, Response)): 194 | if isinstance(content, dict): 195 | content = json.jsonify(content) 196 | resp = make_response(content) 197 | elif isinstance(content, Response): 198 | resp = content 199 | 200 | h = resp.headers 201 | h['Access-Control-Allow-Origin'] = '*' 202 | h['Access-Control-Allow-Methods'] = current_app.make_default_options_response().headers['allow'] 203 | h['Access-Control-Max-Age'] = '21600' 204 | 205 | return resp 206 | 207 | return content 208 | 209 | 210 | if __name__ == '__main__': 211 | flask_optimize = FlaskOptimize() 212 | flask_optimize.optimize('html') 213 | flask_optimize.optimize('json') 214 | flask_optimize.optimize('text') 215 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==2.3.2 2 | htmlmin==0.1.10 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | release = egg_info -RDb '' 3 | 4 | [wheel] 5 | universal = 1 6 | 7 | [pytest] 8 | norecursedirs = .* *.egg *.egg-info env* artwork docs -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sunary' 2 | 3 | 4 | import os 5 | from setuptools import setup, find_packages 6 | from flask_optimize import VERSION 7 | 8 | 9 | def __path(filename): 10 | return os.path.join(os.path.dirname(__file__), filename) 11 | 12 | 13 | with open('README.md') as fo: 14 | readme = fo.read() 15 | 16 | with open('LICENSE') as fo: 17 | license = fo.read() 18 | 19 | with open('CHANGES.md') as fo: 20 | changes = fo.read() 21 | 22 | 23 | setup( 24 | name='flask-optimize', 25 | version=VERSION, 26 | author='Sunary [Nhat Vo Van]', 27 | author_email='v2nhat@gmail.com', 28 | maintainer='Sunary [Nhat Vo Van]', 29 | maintainer_email='v2nhat@gmail.com', 30 | platforms='any', 31 | description='Flask optimization using cache, minify html and compress response', 32 | long_description='Flask optimization using cache, minify html and compress response\n', 33 | license=license, 34 | keywords='flask, optimize, cache, minify html, compress, gzip', 35 | url='https://github.com/sunary/flask-optimize', 36 | packages=find_packages(exclude=['docs', 'tests*']), 37 | install_requires=['Flask>=0.10.1', 38 | 'htmlmin>=0.1.10'], 39 | ) -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sunary' -------------------------------------------------------------------------------- /tests/run_app.py: -------------------------------------------------------------------------------- 1 | __author__ = 'sunary' 2 | 3 | 4 | from flask_optimize import FlaskOptimize 5 | from flask import Flask, request, render_template 6 | import time 7 | 8 | 9 | flask_app = Flask(__name__) 10 | flask_optimize = FlaskOptimize(flask_app) 11 | 12 | 13 | @flask_app.route('/') 14 | @flask_optimize.optimize() 15 | def index(): 16 | return 'Using flask-optimize' 17 | 18 | 19 | @flask_app.route('/html') 20 | @flask_optimize.optimize() 21 | def html(): 22 | return ''' 23 | 24 | 25 | The content of the body element is displayed in your browser. 26 | 27 | 28 | ''' 29 | 30 | @flask_app.route('/text') 31 | @flask_optimize.optimize('text') 32 | def text(): 33 | return ''' 34 | 35 | 36 | Data type response is text, so this content wasn't minified 37 | 38 | 39 | ''' 40 | 41 | @flask_app.route('/json') 42 | @flask_optimize.optimize('json') 43 | def json(): 44 | return {'text': 'anything', 'other_values': [1, 2, 3, 4]} 45 | 46 | 47 | @flask_app.route('/load_svg') 48 | @flask_optimize.optimize() 49 | def load_svg(): 50 | return render_template('load_svg.html') 51 | 52 | 53 | if __name__ == '__main__': 54 | flask_app.run(host='0.0.0.0', port=5372, debug=False) 55 | 56 | time.sleep(5) 57 | func = request.environ.get('werkzeug.server.shutdown') 58 | if func is None: 59 | raise RuntimeError('Not running with the Werkzeug Server') 60 | 61 | func() -------------------------------------------------------------------------------- /tests/templates/load_svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | --------------------------------------------------------------------------------