commit 71a312b30a8f7222f1e2dbd63e38039b942ceaed Author: brettlangdon Date: Mon Nov 28 15:44:48 2016 -0500 Initial version of module diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..72364f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e70bdd6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: python + +python: + - "2.6" + - "2.7" + - "3.3" + - "3.4" + - "3.5" + - "pypy" + +sudo: false + +install: + - pip install flake8 + +script: + - flake8 --max-line-length=120 flask_defer.py && python test_flask_defer.py diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..70777d2 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,10 @@ +Flask-Defer Changelog +=================== + +Version 1.0.0 +------------- + +:Released: 11-28-2016 + +:Changes: + Initial release of package diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e34a441 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Brett Langdon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..7572332 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include CHANGELOG LICENSE README.rst flask_defer.py test_flask_defer.py diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e66fd8f --- /dev/null +++ b/README.rst @@ -0,0 +1,59 @@ +Flask-Defer +========= + +.. image:: https://badge.fury.io/py/flask-defer.svg + :target: https://badge.fury.io/py/flask-defer +.. image:: https://travis-ci.org/brettlangdon/flask-defer.svg?branch=master + :target: https://travis-ci.org/brettlangdon/flask-defer + +Easily register a function to execute at the end of the current request. + +Installation +~~~~~~~~~~~~ + +.. code:: bash + + pip install Flask-Defer + + +Usage +~~~~~ + +.. code:: python + + from flask import Flask + from flask_defer import FlaskDefer, after_request + + app = Flask(__name__) + FlaskDefer(app) + + + def defer_me(name, say_hello=False): + if say_hello: + print 'Saying hello to, {name}'.format(name=name) + + + @app.route('/') + def index(): + print 'Start of request method' + + # Defer `defer_me` until after the current request has finished + after_request(defer_me, 'name', say_hello=True) + + print 'Ending request method' + + return 'Thanks!' + + + if __name__ == '__main__': + app.run() + + +.. code:: bash + + $ python example.py + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + Start of request method + Ending request method + Saying hello to, name + 127.0.0.1 - - [28/Nov/2016 15:41:39] "GET / HTTP/1.1" 200 - diff --git a/example.py b/example.py new file mode 100644 index 0000000..233e14c --- /dev/null +++ b/example.py @@ -0,0 +1,26 @@ +from flask import Flask +from flask_defer import FlaskDefer, after_request + +app = Flask(__name__) +FlaskDefer(app) + + +def defer_me(name, say_hello=False): + if say_hello: + print 'Saying hello to, {name}'.format(name=name) + + +@app.route('/') +def index(): + print 'Start of request method' + + # Defer `defer_me` until after the current request has finished + after_request(defer_me, 'name', say_hello=True) + + print 'Ending request method' + + return 'Thanks!' + + +if __name__ == '__main__': + app.run() diff --git a/flask_defer.py b/flask_defer.py new file mode 100644 index 0000000..34b4bf2 --- /dev/null +++ b/flask_defer.py @@ -0,0 +1,37 @@ +try: + from flask import _app_ctx_stack as stack +except ImportError: + from flask import _request_ctx_stack as stack + +__all__ = ['after_request', 'defer', 'FlaskDefer'] + + +def defer(func, *args, **kwargs): + params = dict(func=func, args=args, kwargs=kwargs) + ctx = stack.top + if not hasattr(ctx, 'deferred_tasks'): + setattr(ctx, 'deferred_tasks', []) + ctx.deferred_tasks.append(params) + + +# Alias `defer` as `after_request` +after_request = defer + + +class FlaskDefer(object): + def __init__(self, app=None): + if app is not None: + self.init_app(app) + + def init_app(self, app): + if hasattr(app, 'teardown_appcontext'): + app.teardown_appcontext(self._execute_deferred) + else: + app.teardown_request(self._execute_deferred) + + def _execute_deferred(self, exception): + ctx = stack.top + if hasattr(ctx, 'deferred_tasks'): + for params in ctx.deferred_tasks: + # DEV: Do not try/except, let these function calls fail + params['func'](*params['args'], **params['kwargs']) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..24ef1d2 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +""" +Flask-Defer +""" +from setuptools import setup + + +def get_long_description(): + with open('README.rst') as f: + rv = f.read() + return rv + + +setup( + name='Flask-Defer', + version='1.0.0', + url='https://github.com/brettlangdon/flask-defer.git', + license='MIT', + author='Brett Langdon', + author_email='me@brett.is', + description='Flask extension to defer task execution under after request teardown', + long_description=get_long_description(), + py_modules=['flask_defer'], + zip_safe=False, + include_package_data=True, + platforms='any', + install_requires=['Flask'], + classifiers=[ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], +) diff --git a/test_flask_defer.py b/test_flask_defer.py new file mode 100644 index 0000000..c08bc7c --- /dev/null +++ b/test_flask_defer.py @@ -0,0 +1,119 @@ +import unittest + +from flask import Flask +from flask_defer import FlaskDefer, defer, stack + + +def deferred_task(name, with_keyword=False): + pass + + +class TestFlaskDefer(unittest.TestCase): + def setUp(self): + self.app = Flask(__name__) + self.defer = FlaskDefer(app=self.app) + + @self.app.route('/') + def test_endpoint(): + defer(deferred_task, 'name', with_keyword=True) + return 'test_endpoint' + + @self.app.route('/multiple') + def test_multiple(): + defer(deferred_task, 'first', with_keyword=True) + defer(deferred_task, 'second', with_keyword=False) + defer(deferred_task, 'third', with_keyword=True) + return 'test_multiple' + + @self.app.route('/extra') + def test_extra_params(): + defer(deferred_task, 'name', 'extra', with_keyword=True, extra_param='param') + return 'test_extra_params' + + @self.app.route('/no-defer') + def test_no_defer(): + return 'test_no_defer' + + def test_deferring_task(self): + with self.app.test_client() as client: + # Make the request so we register the task + client.get('/') + + ctx = stack.top + self.assertTrue(hasattr(ctx, 'deferred_tasks')) + self.assertEqual(len(ctx.deferred_tasks), 1) + + task = ctx.deferred_tasks[0] + self.assertDictEqual(task, dict( + args=('name', ), + func=deferred_task, + kwargs=dict(with_keyword=True), + )) + + # Assert that the deferred tasks aren't shared between requests + with self.app.test_client() as client: + client.get('/no-defer') + + ctx = stack.top + self.assertFalse(hasattr(ctx, 'deferred_tasks')) + + def test_deferring_task_multiple(self): + with self.app.test_client() as client: + # Make the request so we register the task + client.get('/multiple') + + ctx = stack.top + self.assertTrue(hasattr(ctx, 'deferred_tasks')) + self.assertEqual(len(ctx.deferred_tasks), 3) + + task = ctx.deferred_tasks[0] + self.assertDictEqual(task, dict( + args=('first', ), + func=deferred_task, + kwargs=dict(with_keyword=True), + )) + + task = ctx.deferred_tasks[1] + self.assertDictEqual(task, dict( + args=('second', ), + func=deferred_task, + kwargs=dict(with_keyword=False), + )) + + task = ctx.deferred_tasks[2] + self.assertDictEqual(task, dict( + args=('third', ), + func=deferred_task, + kwargs=dict(with_keyword=True), + )) + + def test_deferring_task_no_defer(self): + with self.app.test_client() as client: + # Make the request + client.get('/no-defer') + + ctx = stack.top + self.assertFalse(hasattr(ctx, 'deferred_tasks')) + + def test_deferring_task_extra(self): + # We get a TypeError from the invalid function call + # TODO: There has to be a better/more concise way to test this + with self.assertRaises(TypeError): + with self.app.test_client() as client: + # Make the request so we register the task + client.get('/extra') + + ctx = stack.top + self.assertTrue(hasattr(ctx, 'deferred_tasks')) + self.assertEqual(len(ctx.deferred_tasks), 1) + + task = ctx.deferred_tasks[0] + self.assertDictEqual(task, dict( + args=('name', 'extra'), + func=deferred_task, + kwargs=dict(with_keyword=True, extra_param='param'), + )) + + +if __name__ == '__main__': + unittest.main()