Browse Source

Bump Falcon to 3.0.1 and use python3 instead of python2 (#7036)

Update pypy2 to pypy3
Add waitress server
Add bjoern server

Shut waitress logger

Add bjoern server

Fix bjoern server setup
A Ghulam Zakiy 3 years ago
parent
commit
5741685b81

+ 37 - 14
frameworks/Python/falcon/README.md

@@ -1,4 +1,4 @@
-# Falcon Benchmark Test (ported from Flask example)
+# Falcon Benchmark Test
 
 This is the Falcon portion of a [benchmarking tests suite](../../) 
 comparing a variety of web development platforms.
@@ -10,34 +10,57 @@ the [Python README](../).
 
 ## Description
 
-Falcon API framework (http://falconframework.org)
+[**Falcon**](https://falconframework.org/) is a blazing fast, minimalist Python web API framework for building robust app backends and microservices. The framework works great with both asyncio (ASGI) and gevent/meinheld (WSGI).
+
+Features:
+* ASGI, WSGI, and WebSocket support
+Native asyncio support.
+* No reliance on magic globals for routing and state management.
+* Stable interfaces with an emphasis on backwards-compatibility.
+* Simple API modeling through centralized RESTful routing.
+* Highly-optimized, extensible code base.
+* DRY request processing via middleware components and hooks.
+* Strict adherence to RFCs.
+* Idiomatic HTTP error responses.
+* Straightforward exception handling.
+* WSGI/ASGI testing helpers and mocks.
+* CPython 3.5+ and PyPy 3.5+ support.
 
 ## Infrastructure Software
 
 ### Server
 
-* gunicorn+meinheld on CPython
-* Tornado on PyPy
+* Waitress on CPython3
+* Bjoern on Cpython3
+* Gunicorn + Meinheld on CPython3
+* Gunicorn + Meinheld with Orjson on CPython3
+* Gunicorn Sync on PyPy3
+
+### Database
+
+* Pony ORM [PostgreSQL] - (psycopg2-binary on CPython3, psycopg2cffi on PyPy3)
 
 ## Test Paths & Sources
 
-All of the test implementations are located within a single file ([app.py](app.py)).
+All of the test implementations are located within a single file ([app.py](app.py)) and accessible via http://localhost:8080.
 
 * [JSON Serialization](app.py): "/json"
-* _Single Database Query: N/A_
-* _Multiple Database Queries: N/A_
-* _Fortunes: N/A_
-* _Database Updates: N/A_
+* [Single Database Query](app.py): "/db"
+* [Multiple Database Queries](app.py): "/queries/"
+* [Fortunes](app.py): "/fortunes"
+* [Database Updates](app.py): "/updates/"
 * [Plaintext](app.py): "/plaintext"
 
 ## Get Help
 
 ### Resources
 
-* [Falcon Source Code](https://github.com/falconry/falcon)
+* [Falcon on Github](https://github.com/falconry/falcon)
+* [Docs](https://falcon.readthedocs.io/en/stable/)
+* [FAQ](https://falcon.readthedocs.io/en/stable/user/faq.html#faq)
+* [Wiki](https://github.com/falconry/falcon/wiki)
 
-### [Community](http://falcon.readthedocs.org/en/0.2.0/community/index.html)
+### Community
 
-* `#falconframework` IRC Channel ([irc.freenode.net](https://freenode.net/))
-* Subscribe to email list by emailing falcon[at]librelist.com and 
-following the instructions in the reply.
+* Chatrooms [Falcon for Users](https://gitter.im/falconry/user) and [Falcon for Contributors](https://gitter.im/falconry/dev) @ Gitter
+* [Submit an issue](https://github.com/falconry/falcon/issues)

+ 61 - 19
frameworks/Python/falcon/app.py

@@ -1,34 +1,76 @@
 #!/usr/bin/env python
-import json
-
 import falcon
+from db_orm import session, World, Fortune
+from helpers import load_template, FortuneTuple, generate_ids, sanitize
+from operator import attrgetter
+from random import randint
 
-from email.utils import formatdate
 
-# resource endpoints
+# setup
+wsgi = app = falcon.App()
 
+
+# resource endpoints
 class JSONResource(object):
     def on_get(self, request, response):
-        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
-        json_data = {'message': "Hello, world!"}
-        response.body = json.dumps(json_data)
+        response.media = {'message': "Hello, world!"}
+
+
+class RandomWorld(object):
+    @session(serializable=False)
+    def on_get(self, request, response):
+        wid = randint(1, 10000)
+        world = World[wid]
+        response.media = world.to_dict()
+
+
+class RandomQueries(object):
+    @session(serializable=False)
+    def on_get(self, request, response, **params):
+        num = params.get("num", "1")
+        num = sanitize(num)
+        worlds = [World[ident].to_dict() for ident in generate_ids(num)]
+        response.media = worlds
+
+
+class UpdateQueries(object):
+    @session(serializable=False)
+    def on_get(self, request, response, **params):
+        num = params.get("num", "1")
+        num = sanitize(num)
+        ids = generate_ids(num)
+        ids.sort()
+        worlds = []
+        for item in ids:
+            world = World[item]
+            world.randomNumber = randint(1, 10000)
+            worlds.append({"id": world.id, "randomNumber": world.randomNumber})
+        response.media = worlds
+
+
+class Fortunes(object):
+    _template = load_template()
+
+    @session(serializable=False)
+    def on_get(self, request, response):
+        fortunes = [FortuneTuple(id=f.id, message=f.message) for f in Fortune.select()]
+        fortunes.append(FortuneTuple(id=0, message="Additional fortune added at request time."))
+        fortunes.sort(key=attrgetter("message"))
+        content = self._template.render(fortunes=fortunes)
+        response.content_type = falcon.MEDIA_HTML
+        response.text = content
 
 
 class PlaintextResource(object):
     def on_get(self, request, response):
-        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
-        response.set_header('Content-Type', 'text/plain')
-        response.body = b'Hello, world!'
+        response.content_type = falcon.MEDIA_TEXT
+        response.text = 'Hello, world!'
 
-# setup
 
-app = falcon.API()
+# register resources
 app.add_route("/json", JSONResource())
+app.add_route("/db", RandomWorld())
+app.add_route("/queries/{num}", RandomQueries())
+app.add_route("/updates/{num}", UpdateQueries())
+app.add_route("/fortunes", Fortunes())
 app.add_route("/plaintext", PlaintextResource())
-
-# entry point for debugging
-if __name__ == "__main__":
-    from wsgiref import simple_server
-
-    httpd = simple_server.make_server('localhost', 8080, app)
-    httpd.serve_forever()

+ 104 - 0
frameworks/Python/falcon/app_bjoern.py

@@ -0,0 +1,104 @@
+#!/usr/bin/env python
+import bjoern
+import falcon
+import re
+from email.utils import formatdate
+from db_orm import session, World, Fortune
+from helpers import load_template, FortuneTuple, generate_ids, sanitize
+from operator import attrgetter
+from random import randint
+
+
+# setup
+wsgi = app = falcon.App()
+
+# Note:
+# Bjoern doesn't provide any additional response headers like Date and Server
+# so we need to provide them manually.
+bjoern_version = [i for i in open('requirements-bjoern.txt', 'r') if re.search('bjoern', i)][0].strip().split('==')
+server_info = '{}/{}'.format(*bjoern_version).title()
+
+
+# resource endpoints
+class JSONResource(object):
+    def on_get(self, request, response):
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.media = {'message': "Hello, world!"}
+
+
+class RandomWorld(object):
+    @session(serializable=False)
+    def on_get(self, request, response):
+        wid = randint(1, 10000)
+        world = World[wid]
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.media = world.to_dict()
+
+
+class RandomQueries(object):
+    @session(serializable=False)
+    def on_get(self, request, response, **params):
+        num = params.get("num", "1")
+        num = sanitize(num)
+        worlds = [World[ident].to_dict() for ident in generate_ids(num)]
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.media = worlds
+
+
+class UpdateQueries(object):
+    @session(serializable=False)
+    def on_get(self, request, response, **params):
+        num = params.get("num", "1")
+        num = sanitize(num)
+        ids = generate_ids(num)
+        ids.sort()
+        worlds = []
+        for item in ids:
+            world = World[item]
+            world.randomNumber = randint(1, 10000)
+            worlds.append({"id": world.id, "randomNumber": world.randomNumber})
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.media = worlds
+
+
+class Fortunes(object):
+    _template = load_template()
+
+    @session(serializable=False)
+    def on_get(self, request, response):
+        fortunes = [FortuneTuple(id=f.id, message=f.message) for f in Fortune.select()]
+        fortunes.append(FortuneTuple(id=0, message="Additional fortune added at request time."))
+        fortunes.sort(key=attrgetter("message"))
+        content = self._template.render(fortunes=fortunes)
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.content_type = falcon.MEDIA_HTML
+        response.text = content
+
+
+class PlaintextResource(object):
+    def on_get(self, request, response):
+        response.set_header('Date', formatdate(timeval=None, localtime=False, usegmt=True))
+        response.set_header('Server', server_info)
+        response.content_type = falcon.MEDIA_TEXT
+        response.text = 'Hello, world!'
+
+
+# register resources
+app.add_route("/json", JSONResource())
+app.add_route("/db", RandomWorld())
+app.add_route("/queries/{num}", RandomQueries())
+app.add_route("/updates/{num}", UpdateQueries())
+app.add_route("/fortunes", Fortunes())
+app.add_route("/plaintext", PlaintextResource())
+
+
+if __name__ == "__main__":
+    host = '0.0.0.0'
+    port = 8080
+
+    bjoern.run(wsgi, host=host, port=port)

+ 14 - 0
frameworks/Python/falcon/app_orjson.py

@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+import orjson
+from falcon import media
+from app import wsgi
+
+
+# custom JSON handler
+JSONHandler = media.JSONHandler(dumps=orjson.dumps, loads=orjson.loads)
+extra_handlers = {
+    "application/json": JSONHandler,
+    "application/json; charset=UTF-8": JSONHandler
+}
+wsgi.req_options.media_handlers.update(extra_handlers)
+wsgi.resp_options.media_handlers.update(extra_handlers)

+ 21 - 0
frameworks/Python/falcon/app_waitress.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+from waitress import serve
+from app import wsgi
+from gunicorn_conf import bind, keepalive, workers
+import logging
+
+
+if __name__ == "__main__":
+    logging.basicConfig()
+    logging.getLogger().setLevel(logging.CRITICAL)
+    logging.disable(True)
+
+    serve(
+        app=wsgi,
+        listen=bind,
+        log_socket_errors=False,
+        threads=workers,
+        expose_tracebacks=False,
+        connection_limit=128,
+        channel_timeout=keepalive,
+        _quiet=True)

+ 78 - 20
frameworks/Python/falcon/benchmark_config.json

@@ -3,59 +3,117 @@
   "tests": [{
     "default": {
       "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
       "plaintext_url": "/plaintext",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Micro",
-      "database": "None",
+      "database": "Postgres",
       "framework": "Falcon",
       "language": "Python",
-      "flavor": "Python2",
-      "orm": "Raw",
-      "platform": "None",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "WSGI",
       "webserver": "Meinheld",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "Falcon",
-      "notes": "CPython 2",
+      "display_name": "Falcon [Meinheld]",
+      "notes": "cythonized",
+      "versus": "wsgi"
+    },
+    "bjoern": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "Falcon",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "WSGI",
+      "webserver": "bjoern",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Falcon [Bjoern]",
+      "notes": "",
+      "versus": "wsgi"
+    },
+    "waitress": {
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "Falcon",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "WSGI",
+      "webserver": "waitress",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Falcon [Waitress]",
+      "notes": "",
       "versus": "wsgi"
     },
-    "py3": {
+    "orjson": {
       "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
       "plaintext_url": "/plaintext",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Micro",
-      "database": "None",
+      "database": "Postgres",
       "framework": "Falcon",
       "language": "Python",
       "flavor": "Python3",
-      "orm": "Raw",
-      "platform": "None",
+      "orm": "Full",
+      "platform": "WSGI",
       "webserver": "Meinheld",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "Falcon",
-      "notes": "CPython 3",
+      "display_name": "Falcon [Meinheld-Orjson]",
+      "notes": "cythonized+orjson",
       "versus": "wsgi"
     },
-    "pypy2": {
+    "pypy3": {
       "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries/",
+      "update_url": "/updates/",
+      "fortune_url": "/fortunes",
       "plaintext_url": "/plaintext",
       "port": 8080,
       "approach": "Realistic",
       "classification": "Micro",
-      "database": "None",
+      "database": "Postgres",
       "framework": "Falcon",
       "language": "Python",
-      "flavor": "PyPy2",
-      "orm": "Raw",
-      "platform": "None",
-      "webserver": "Tornado",
+      "flavor": "PyPy3",
+      "orm": "Full",
+      "platform": "WSGI",
+      "webserver": "Gunicorn",
       "os": "Linux",
       "database_os": "Linux",
-      "display_name": "Falcon",
-      "notes": "PyPy2",
+      "display_name": "Falcon [Pypy3]",
+      "notes": "",
       "versus": "wsgi"
     }
   }]

+ 98 - 22
frameworks/Python/falcon/config.toml

@@ -2,40 +2,116 @@
 name = "falcon"
 
 [main]
-urls.plaintext = "/plaintext"
-urls.json = "/json"
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
 approach = "Realistic"
 classification = "Micro"
-database = "None"
-database_os = "Linux"
-os = "Linux"
-orm = "Raw"
-platform = "None"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "Python3"
+orm = "Full"
+platform = "WSGI"
 webserver = "Meinheld"
+os = "Linux"
+database.os = "Linux"
+display.name = "Falcon [Meinheld]"
+notes = "cythonized"
+versus = "wsgi"
+
+[bjoern]
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "Python3"
+orm = "Full"
+platform = "WSGI"
+webserver = "bjoern"
+os = "Linux"
+database.os = "Linux"
+display.name = "Falcon [Bjoern]"
+notes = ""
 versus = "wsgi"
 
-[py3]
-urls.plaintext = "/plaintext"
-urls.json = "/json"
+[waitress]
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
 approach = "Realistic"
 classification = "Micro"
-database = "None"
-database_os = "Linux"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "Python3"
+orm = "Full"
+platform = "WSGI"
+webserver = "waitress"
 os = "Linux"
-orm = "Raw"
-platform = "None"
+database.os = "Linux"
+display.name = "Falcon [Waitress]"
+notes = ""
+versus = "wsgi"
+
+[orjson]
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "Python3"
+orm = "Full"
+platform = "WSGI"
 webserver = "Meinheld"
+os = "Linux"
+database.os = "Linux"
+display.name = "Falcon [Meinheld-Orjson]"
+notes = "cythonized+orjson"
 versus = "wsgi"
 
-[pypy2]
-urls.plaintext = "/plaintext"
-urls.json = "/json"
+[pypy3]
+json.url = "/json"
+db.url = "/db"
+query.url = "/queries/"
+update.url = "/updates/"
+fortune.url = "/fortunes"
+plaintext.url = "/plaintext"
+port = 8080
 approach = "Realistic"
 classification = "Micro"
-database = "None"
-database_os = "Linux"
+database = "Postgres"
+framework = "Falcon"
+language = "Python"
+flavor = "PyPy3"
+orm = "Full"
+platform = "WSGI"
+webserver = "Gunicorn"
 os = "Linux"
-orm = "Raw"
-platform = "None"
-webserver = "Tornado"
+database.os = "Linux"
+display.name = "Falcon [Pypy3]"
+notes = ""
 versus = "wsgi"

+ 45 - 0
frameworks/Python/falcon/db_orm.py

@@ -0,0 +1,45 @@
+import sys
+import os
+from pony.orm import (
+    Database,
+    PrimaryKey,
+    Required,
+    db_session as session
+)
+
+
+_is_pypy = hasattr(sys, "pypy_version_info")
+
+if _is_pypy:
+    from psycopg2cffi import compat
+
+    compat.register()
+
+db = Database()
+db.bind(
+    provider="postgres",
+    host="tfb-database",
+    port=5432,
+    user=os.getenv("PGUSER", "benchmarkdbuser"),
+    password=os.getenv("PSPASS", "benchmarkdbpass"),
+    database="hello_world",
+)
+
+
+class World(db.Entity):
+    _table_ = "world"
+    id = PrimaryKey(int)
+    randomNumber = Required(int, column="randomnumber")
+
+    def to_dict(self):
+        """Return object data in easily serializeable format"""
+        return {"id": self.id, "randomNumber": self.randomNumber}
+
+
+class Fortune(db.Entity):
+    _table_ = "fortune"
+    id = PrimaryKey(int, auto=True)
+    message = Required(str)
+
+
+db.generate_mapping(create_tables=False)

+ 10 - 0
frameworks/Python/falcon/falcon-bjoern.dockerfile

@@ -0,0 +1,10 @@
+FROM python:3.8-buster
+
+RUN apt-get update; apt-get install libpq-dev python3-dev libev-dev -y
+COPY ./ /falcon
+WORKDIR /falcon
+RUN pip3 install -U pip; pip3 install -r /falcon/requirements-bjoern.txt
+
+EXPOSE 8080
+
+CMD ["python3", "app_bjoern.py"]

+ 13 - 0
frameworks/Python/falcon/falcon-orjson.dockerfile

@@ -0,0 +1,13 @@
+FROM python:3.8-buster
+
+RUN apt-get update; apt-get install libpq-dev python3-dev -y
+COPY ./ /falcon
+WORKDIR /falcon
+RUN pip3 install -U pip; \
+    pip3 install cython==0.29.26; \
+    pip3 install falcon==3.0.1 --no-binary :all:; \
+    pip3 install -r /falcon/requirements.txt
+
+EXPOSE 8080
+
+CMD ["gunicorn", "app_orjson:wsgi", "-c", "gunicorn_conf.py"]

+ 0 - 12
frameworks/Python/falcon/falcon-py3.dockerfile

@@ -1,12 +0,0 @@
-FROM python:3.6.6-stretch
-
-WORKDIR /falcon
-COPY app.py app.py
-COPY gunicorn_conf.py gunicorn_conf.py
-COPY requirements.txt requirements.txt
-
-RUN pip3 install -r requirements.txt
-
-EXPOSE 8080
-
-CMD ["gunicorn", "app:app", "-c", "gunicorn_conf.py"]

+ 0 - 12
frameworks/Python/falcon/falcon-pypy2.dockerfile

@@ -1,12 +0,0 @@
-FROM pypy:2-5.10
-
-WORKDIR /falcon
-COPY app.py app.py
-COPY gunicorn_conf.py gunicorn_conf.py
-COPY requirements-pypy.txt requirements-pypy.txt
-
-RUN pip install -r requirements-pypy.txt
-
-EXPOSE 8080
-
-CMD ["gunicorn", "app:app", "-c", "gunicorn_conf.py"]

+ 10 - 0
frameworks/Python/falcon/falcon-pypy3.dockerfile

@@ -0,0 +1,10 @@
+FROM pypy:3.8-7.3
+
+RUN apt-get update; apt-get install libpq-dev python3-dev -y
+COPY ./ /falcon
+WORKDIR /falcon
+RUN pip3 install -U pip; pip3 install -r /falcon/requirements-pypy.txt
+
+EXPOSE 8080
+
+CMD ["gunicorn", "app:wsgi", "-c", "gunicorn_conf.py"]

+ 10 - 0
frameworks/Python/falcon/falcon-waitress.dockerfile

@@ -0,0 +1,10 @@
+FROM python:3.8-buster
+
+RUN apt-get update; apt-get install libpq-dev python3-dev -y
+COPY ./ /falcon
+WORKDIR /falcon
+RUN pip3 install -U pip; pip3 install -r /falcon/requirements.txt
+
+EXPOSE 8080
+
+CMD ["python", "app_waitress.py"]

+ 8 - 7
frameworks/Python/falcon/falcon.dockerfile

@@ -1,12 +1,13 @@
-FROM python:2.7.15-stretch
+FROM python:3.8-buster
 
+RUN apt-get update; apt-get install libpq-dev python3-dev -y
+COPY ./ /falcon
 WORKDIR /falcon
-COPY app.py app.py
-COPY gunicorn_conf.py gunicorn_conf.py
-COPY requirements.txt requirements.txt
-
-RUN pip install -r requirements.txt
+RUN pip3 install -U pip; \
+    pip3 install cython==0.29.26; \
+    pip3 install falcon==3.0.1 --no-binary :all:; \
+    pip3 install -r /falcon/requirements.txt
 
 EXPOSE 8080
 
-CMD ["gunicorn", "app:app", "-c", "gunicorn_conf.py"]
+CMD ["gunicorn", "app:wsgi", "-c", "gunicorn_conf.py"]

+ 3 - 4
frameworks/Python/falcon/gunicorn_conf.py

@@ -2,11 +2,11 @@ import multiprocessing
 import os
 import sys
 
+
 _is_pypy = hasattr(sys, 'pypy_version_info')
 _is_travis = os.environ.get('TRAVIS') == 'true'
 
-# falcon only implements json and plain. Not wait DB.
-workers = multiprocessing.cpu_count()  # *3
+workers = int(multiprocessing.cpu_count() * 1.5)
 if _is_travis:
     workers = 2
 
@@ -16,7 +16,7 @@ errorlog = '-'
 pidfile = 'gunicorn.pid'
 
 if _is_pypy:
-    worker_class = "tornado"
+    worker_class = "sync"
 else:
     worker_class = "meinheld.gmeinheld.MeinheldWorker"
 
@@ -24,4 +24,3 @@ else:
         # Disalbe access log
         import meinheld.server
         meinheld.server.set_access_logger(None)
-

+ 35 - 0
frameworks/Python/falcon/helpers.py

@@ -0,0 +1,35 @@
+import jinja2
+from pathlib import Path
+from collections import namedtuple
+from random import randint
+
+
+def sanitize(query):
+    if not query.isnumeric():
+        res = 1
+    else:
+        query = int(query)
+        if query < 1:
+            res = 1
+        elif query > 500:
+            res = 500
+        else:
+            res = query
+    return res
+
+
+def generate_ids(num_queries):
+    ids = {randint(1, 10000) for _ in range(num_queries)}
+    while len(ids) < num_queries:
+        ids.add(randint(1, 10000))
+    return list(sorted(ids))
+
+
+def load_template():
+    path = Path("templates", "fortune.html")
+    with open(str(path), "r") as template_file:
+        template_text = template_file.read()
+        return jinja2.Template(template_text)
+
+
+FortuneTuple = namedtuple("Fortune", ["id", "message"])

+ 2 - 0
frameworks/Python/falcon/requirements-bjoern.txt

@@ -0,0 +1,2 @@
+-r requirements.txt
+bjoern==3.1.0

+ 5 - 7
frameworks/Python/falcon/requirements-pypy.txt

@@ -1,7 +1,5 @@
-falcon==1.4.1
-greenlet==0.4.14
-gunicorn==19.9.0
-python-mimeparse==1.6.0
-readline==6.2.4.1
-six==1.11.0
-tornado==4.5.3
+falcon==3.0.1
+gunicorn==20.1.0
+jinja2==3.0.3
+pony==0.7.14
+psycopg2cffi==2.9.0; implementation_name=='pypy'

+ 9 - 7
frameworks/Python/falcon/requirements.txt

@@ -1,7 +1,9 @@
-Cython==0.27.3
-falcon==1.4.1
-greenlet==0.4.14
-gunicorn==19.9.0
-meinheld==0.6.1
-python-mimeparse==1.6.0
-six==1.11.0
+Cython==0.29.26
+falcon==3.0.1
+gunicorn==20.1.0
+jinja2==3.0.3
+meinheld==1.0.2
+orjson==3.6.5
+pony==0.7.14
+psycopg2-binary==2.9.3; implementation_name=='cpython'
+waitress==2.0.0

+ 10 - 0
frameworks/Python/falcon/templates/fortune.html

@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head><title>Fortunes</title></head>
+<body>
+<table>
+<tr><th>id</th><th>message</th></tr>
+{% for fortune in fortunes %}<tr><td>{{ fortune[0] }}</td><td>{{ fortune[1]|e }}</td></tr>
+{% endfor %}</table>
+</body>
+</html>