├── .gitignore ├── LICENSE ├── README.rst ├── example.py ├── flask_aggregator.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.pyc 5 | *~ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Guillaume Gelin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-Aggregator 2 | ================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/flask-aggregator.svg 5 | 6 | Batch the GET requests to your API into a single POST. Save requests latency and 7 | reduce REST chatiness. 8 | 9 | I was inspired by `this article 10 | `_ 11 | from 3scale, and by `their NGINX aggregator 12 | `_ - but I wanted something simpler. 13 | 14 | 15 | What does it do? 16 | ---------------- 17 | 18 | Flask-Aggregator adds an endpoint to your Flask application that handles 19 | multiple GET requests in a single POST, and returns the response of each GET 20 | request in a JSON stream. 21 | 22 | 23 | What does that mean? 24 | -------------------- 25 | 26 | It means that instead of sending multiple GET requests: 27 | 28 | .. code-block:: sh 29 | 30 | -> GET /route1 31 | <- answer1 32 | -> GET /route2 33 | <- answer2 34 | -> GET /route3 35 | <- answer3 36 | 37 | 38 | You can now just send a single POST that aggregates them all: 39 | 40 | .. code-block:: sh 41 | 42 | -> POST /aggregate ["/route1", "/route2", "/route3"] 43 | <- { 44 | "/route1": answer1, 45 | <- "/route2": answer2, 46 | <- "/route3": answer3 47 | } 48 | 49 | 50 | Why? 51 | ---- 52 | 53 | Mobile networks. 54 | 55 | 56 | How to install? 57 | --------------- 58 | 59 | .. code-block:: sh 60 | 61 | $ pip install flask-aggregator 62 | 63 | 64 | How to setup my application? 65 | ---------------------------- 66 | 67 | .. code-block:: python 68 | 69 | from flask import Flask 70 | from flask_aggregator import Aggregator 71 | 72 | app = Flask(__name__) 73 | Aggregator(app=app, endpoint="/batch") 74 | 75 | 76 | How to aggregate? 77 | ----------------- 78 | 79 | .. code-block:: sh 80 | 81 | $ python example.py 82 | [go to another shell] 83 | $ curl -H "Content-type: application/json" -X POST 127.0.0.1:5000/batch \ 84 | --data-raw '["/hello/world", "/hello/ramnes?question=Sup?"]' 85 | { 86 | "/hello/world": "Hello, world!", 87 | "/hello/ramnes?question=Sup?": "Hello, ramnes! Sup?" 88 | } 89 | 90 | 91 | Is it ready for production yet? 92 | ------------------------------- 93 | 94 | Not really. 95 | 96 | As of today, Flask-Aggregator executes the aggregated requests in a 97 | synchronous manner, which makes it only useful if latency is a real issue and 98 | response time is not, and that more than N requests are sent at the same time, 99 | where N is maximum number of concurrent requests on user's client. 100 | 101 | Also, it has limitations such has: 102 | 103 | * no automatic caching mechanism browser-side, since it uses a POST request 104 | * no header support at all for now, which means no cookie, etag, or whatever 105 | * no other HTTP verb than GET is supported for now 106 | 107 | Last but not least, chances are high that a lot of corner cases are not handled. 108 | 109 | 110 | License 111 | ------- 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from flask_aggregator import Aggregator 3 | 4 | app = Flask(__name__) 5 | Aggregator(app=app, endpoint="/batch") 6 | 7 | 8 | @app.route("/hello/", methods=["GET"]) 9 | def hello_name(name): 10 | response = "Hello, {}!".format(name) 11 | question = request.args.get("question") 12 | if question: 13 | response += " " + question 14 | return response 15 | 16 | 17 | if __name__ == "__main__": 18 | app.run() 19 | -------------------------------------------------------------------------------- /flask_aggregator.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import request, Response 4 | from werkzeug.exceptions import BadRequest 5 | from werkzeug.test import EnvironBuilder 6 | 7 | 8 | class Aggregator(object): 9 | 10 | def __init__(self, app=None, endpoint=None): 11 | self.url_map = {} 12 | self.endpoint = endpoint or "/aggregate" 13 | if app: 14 | self.init_app(app) 15 | 16 | def init_app(self, app): 17 | app.add_url_rule(self.endpoint, view_func=self.post, methods=["POST"], 18 | defaults={"app": app}) 19 | 20 | def get_response(self, app, route): 21 | query_string = "" 22 | if '?' in route: 23 | route, query_string = route.split('?', 1) 24 | 25 | builder = EnvironBuilder(path=route, query_string=query_string) 26 | app.request_context(builder.get_environ()).push() 27 | return app.dispatch_request() 28 | 29 | def post(self, app): 30 | try: 31 | data = request.data.decode('utf-8') 32 | routes = json.loads(data) 33 | if not isinstance(routes, list): 34 | raise TypeError 35 | except (ValueError, TypeError) as e: 36 | raise BadRequest("Can't get requests list.") 37 | 38 | def __generate(): 39 | data = None 40 | for route in routes: 41 | yield data + ', ' if data else '{' 42 | response = self.get_response(app, route) 43 | json_response = json.dumps(response) 44 | data = '"{}": {}'.format(route, json_response) 45 | yield data + '}' 46 | 47 | return Response(__generate(), mimetype='application/json') 48 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | def get_description(): 5 | with open("README.rst") as file: 6 | return file.read() 7 | 8 | 9 | setup( 10 | name='Flask-Aggregator', 11 | version='0.2.0', 12 | url='https://github.com/ramnes/flask-aggregator', 13 | license='MIT', 14 | author='Guillaume Gelin', 15 | author_email='contact@ramnes.eu', 16 | description='Batch the GET requests to your REST API into a single POST', 17 | long_description=get_description(), 18 | py_modules=['flask_aggregator'], 19 | include_package_data=True, 20 | zip_safe=False, 21 | platforms='any', 22 | install_requires=['Flask'], 23 | classifiers=[ 24 | 'Environment :: Web Environment', 25 | 'Intended Audience :: Developers', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 29 | 'Topic :: Software Development :: Libraries :: Python Modules' 30 | ] 31 | ) 32 | --------------------------------------------------------------------------------