diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..82c33ad --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,29 @@ +name: Build + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.4, 3.5, 3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.setup.version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Lint with flake8 + run: | + pip install flake8 + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=13 --max-line-length=127 --statistics + - name: Install dependencies + run: | + pip install -r requirements.txt -r requirements.test.txt + - name: Test with pytest + run: | + pytest tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3ac6e56..fed7de9 100644 --- a/.gitignore +++ b/.gitignore @@ -108,9 +108,9 @@ celerybeat.pid # Environments .env -.venv +.venv* env/ -venv/ +venv*/ ENV/ env.bak/ venv.bak/ diff --git a/MANIFEST.in b/MANIFEST.in index dc79a18..5c503f7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ include MANIFEST.in include requirements.txt +include requirements.test.txt +include tests * include README.md include LICENSE diff --git a/requirements.test.txt b/requirements.test.txt new file mode 100644 index 0000000..0275095 --- /dev/null +++ b/requirements.test.txt @@ -0,0 +1,3 @@ +requests +pytest +pytest-benchmark \ No newline at end of file diff --git a/setup.py b/setup.py index 60eace4..0833761 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ def parse_requirements(filename): lineiter = (line.strip() for line in open(filename)) return [line for line in lineiter if line and not line.startswith("#")] + setup( name='Flask-Inflate', version='0.3', @@ -26,6 +27,8 @@ def parse_requirements(filename): include_package_data=True, platforms='any', install_requires=parse_requirements('requirements.txt'), + test_suite="tests", + tests_require=parse_requirements('requirements.test.txt'), classifiers=[ 'Framework :: Flask', 'Environment :: Web Environment', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_performance.py b/tests/test_performance.py new file mode 100644 index 0000000..287d694 --- /dev/null +++ b/tests/test_performance.py @@ -0,0 +1,80 @@ +import gzip +import json +import random + +import pytest +from flask import Flask, request, jsonify +from flask_inflate import inflate + +app = Flask(__name__) + + +@app.route('/naive', methods=["POST"]) +def without_inflate(): + json_payload = gzip.decompress(request.get_data()) if request.content_encoding == "gzip" else request.get_data(as_text=True) + json.loads(json_payload) + return jsonify("OK") + + +@app.route('/inflate', methods=["POST"]) +@inflate +def with_inflate(): + # I could've used .get_json() or .json here but just to be explicit about the fact it's doing the same + # as the naive route + json_payload = request.get_data(as_text=True) + json.loads(json_payload) + return jsonify("OK") + + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + + +def generate_compressed_payload(n_keys): + random_json = { + "random_text{}".format(i): ' '.join(random.choice(["some", "repeating", "words"]) for _ in range(150)) + for i in range(n_keys) + } + + raw_json = json.dumps(random_json).encode("utf-8") + return gzip.compress(raw_json) + + +@pytest.fixture +def gzipped_payload(): + yield generate_compressed_payload(1000) + + +def test_inflate(gzipped_payload, client, benchmark): + send_request(client, gzipped_payload, False, True) # warmup + result = benchmark.pedantic(send_request, args=(client, gzipped_payload), kwargs={"naive": False, "compressed": True}, rounds=1000) + assert result == "OK" + + +def test_naive(gzipped_payload, client, benchmark): + send_request(client, gzipped_payload, True, True) + result = benchmark.pedantic(send_request, args=(client, gzipped_payload), kwargs={"naive": True, "compressed": True}, rounds=1000) + assert result == "OK" + + +def send_request(_client, payload, naive=False, compressed=True): + headers = {"Content-Type": "application/json"} + if compressed: + headers["Content-Encoding"] = "gzip" + + path = 'naive' if naive else 'inflate' + return _client.post('http://localhost:5000/{}'.format(path), data=payload, headers=headers).json + + +if __name__ == '__main__': + import cProfile + + with app.test_client() as client: + payload = generate_compressed_payload(1000) + + send_request(client, payload, True, True) # warmup + send_request(client, payload, False, True) # warmup + cProfile.run('send_request(client, payload, True, True)', sort='cumtime') + cProfile.run('send_request(client, payload, False, True)', sort='cumtime')