├── .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 |
--------------------------------------------------------------------------------