Sfoglia il codice sorgente

[Python] Add Microdot (#7561)

Miguel Grinberg 2 anni fa
parent
commit
b9fd8d170c

+ 58 - 0
frameworks/Python/microdot/README.md

@@ -0,0 +1,58 @@
+# Microdot Benchmarking Test
+
+This is the [Microdot](https://github.com/miguelgrinberg/microdot) portion of a [benchmarking tests suite](../../) comparing a variety of web development platforms.
+
+The information below is specific to Microdot.
+For further guidance, review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
+Also note that there is additional information provided in the [Python README](../).
+
+### Test Source Code
+
+* [JSON](app_sync.py#L60)
+* [JSON-async](app_async.py#L60)
+* [PLAINTEXT](app_sync.py#L102)
+* [PLAINTEXT-async](app_async.py#L102)
+* [DB](app_sync.py#L65)
+* [DB-async](app_async.py#L65)
+* [QUERY](app_sync.py#L73)
+* [QUERY-async](app_async.py#L83)
+* [CACHED QUERY](app_sync.py#L112)
+* [CACHED_QUERY-async](app_async.py#L112)
+* [UPDATE](app_sync.py#L89)
+* [UPDATE-async](app_async.py#L89)
+* [FORTUNES](app_sync.py#L80)
+* [FORTUNES-async](app_async.py#L80)
+
+## Resources
+
+* [Microdot](https://github.com/miguelgrinberg/microdot)
+* [Alchemical](https://github.com/miguelgrinberg/alchemical)
+
+## Test URLs
+### JSON
+
+http://localhost:8080/json
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/queries?queries=
+
+### CACHED QUERY
+
+http://localhost:8080/cached_query?count=
+
+### UPDATE
+
+http://localhost:8080/update?queries=
+
+### FORTUNES
+
+http://localhost:8080/fortunes

+ 117 - 0
frameworks/Python/microdot/app_async.py

@@ -0,0 +1,117 @@
+#!/usr/bin/env python
+from datetime import datetime
+import os
+from random import randint
+
+from alchemical.aio import Alchemical
+import sqlalchemy as sqla
+from asyncache import cached
+from cachetools.keys import hashkey
+
+from microdot_asgi import Microdot
+from microdot_jinja import render_template
+
+app = Microdot()
+db = Alchemical(os.environ['DATABASE_URL'])
+
+
+class World(db.Model):
+    __tablename__ = "world"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    randomnumber = sqla.Column(sqla.Integer)
+
+    def to_dict(self):
+        return {"id": self.id, "randomNumber": self.randomnumber}
+
+
+class CachedWorld(db.Model):
+    __tablename__ = "cachedworld"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    randomnumber = sqla.Column(sqla.Integer)
+
+    def to_dict(self):
+        return {"id": self.id, "randomNumber": self.randomnumber}
+
+
+class Fortune(db.Model):
+    __tablename__ = "fortune"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    message = sqla.Column(sqla.String)
+
+
+def get_num_queries(request, name="queries"):
+    try:
+        num_queries = request.args.get(name, 1, type=int)
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+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(ids)
+
+
[email protected]("/json")
+async def test_json(request):
+    return {"message": "Hello, World!"}
+
+
[email protected]("/db")
+async def test_db(request):
+    id = randint(1, 10000)
+    async with db.Session() as session:
+        world = await session.get(World, id)
+    return world.to_dict()
+
+
[email protected]("/queries")
+async def test_queries(request):
+    async with db.Session() as session:
+        worlds = [(await session.get(World, id)).to_dict() for id in generate_ids(get_num_queries(request))]
+    return worlds
+
+
[email protected]("/fortunes")
+async def test_fortunes(request):
+    async with db.Session() as session:
+        fortunes = list(await session.scalars(Fortune.select()))
+    fortunes.append(Fortune(id=0, message="Additional fortune added at request time."))
+    fortunes.sort(key=lambda f: f.message)
+    return render_template("fortunes.html", fortunes=fortunes), {'Content-Type': 'text/html; charset=utf-8'}
+
+
[email protected]("/updates")
+async def test_updates(request):
+    worlds = []
+    ids = generate_ids(get_num_queries(request))
+    ids.sort()  # to avoid deadlocks
+    async with db.begin() as session:
+        for id in ids:
+            world = await session.get(World, id)
+            world.randomnumber = (randint(1, 9999) + world.randomnumber - 1) % 10000 + 1
+            worlds.append(world.to_dict())
+    return worlds
+
+
[email protected]("/plaintext")
+async def test_plaintext(request):
+    return b"Hello, World!"
+
+
+@cached(cache={}, key=lambda session, id: hashkey(id))
+async def get_cached_world(session, id):
+    return (await session.get(World, id)).to_dict()
+
+
[email protected]("/cached-queries")
+async def test_cached_queries(request):
+    async with db.Session() as session:
+        worlds = [await get_cached_world(session, id) for id in generate_ids(get_num_queries(request, 'count'))]
+    return worlds

+ 124 - 0
frameworks/Python/microdot/app_async_raw.py

@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+from datetime import datetime
+import os
+from random import randint
+
+import asyncpg
+from asyncache import cached
+from cachetools.keys import hashkey
+
+from microdot_asgi import Microdot
+from microdot_jinja import render_template
+
+app = Microdot()
+get_world_sql = 'SELECT id, randomnumber FROM world WHERE id = $1'
+update_world_sql = 'UPDATE world SET randomnumber = $1 WHERE id = $2'
+fortune_sql = 'SELECT * FROM fortune'
+db = None
+
+async def asgi(scope, receive, send):
+    if scope['type'] == 'lifespan':
+        while True:
+            message = await receive()
+            if message['type'] == 'lifespan.startup':
+                global db, get_world_stmt, update_world_stmt, fortune_stmt
+                db = await asyncpg.create_pool(os.environ['DATABASE_URL'])
+                await send({'type': 'lifespan.startup.complete'})
+            elif message['type'] == 'lifespan.shutdown':
+                db.close()
+                await send({'type': 'lifespan.shutdown.complete'})
+                return
+    else:
+        return await app(scope, receive, send)
+
+
+def get_num_queries(request, name="queries"):
+    try:
+        num_queries = request.args.get(name, 1, type=int)
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+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(ids)
+
+
[email protected]("/json")
+async def test_json(request):
+    return {"message": "Hello, World!"}
+
+
[email protected]("/db")
+async def test_db(request):
+    id = randint(1, 10000)
+    async with db.acquire() as conn:
+        result = await conn.fetchrow(get_world_sql, id)
+        world = {'id': result[0], 'randomNumber': result[1]}
+    return world
+
+
+async def get_world(stmt, id):
+    result = await stmt.fetchrow(id)
+    return {'id': result[0], 'randomNumber': result[1]}
+
+
[email protected]("/queries")
+async def test_queries(request):
+    async with db.acquire() as conn:
+        stmt = await conn.prepare(get_world_sql)
+        worlds = [await get_world(stmt, id) for id in generate_ids(get_num_queries(request))]
+    return worlds
+
+
[email protected]("/fortunes")
+async def test_fortunes(request):
+    async with db.acquire() as conn:
+        fortunes = list(await conn.fetch(fortune_sql))
+    fortunes.append((0, "Additional fortune added at request time."))
+    fortunes.sort(key=lambda f: f[1])
+    return render_template("fortunes_raw.html", fortunes=fortunes), {'Content-Type': 'text/html; charset=utf-8'}
+
+
[email protected]("/updates")
+async def test_updates(request):
+    worlds = []
+    updated_worlds = []
+    ids = generate_ids(get_num_queries(request))
+    ids.sort()  # to avoid deadlocks
+    async with db.acquire() as conn:
+        get_stmt = await conn.prepare(get_world_sql)
+        update_stmt = await conn.prepare(update_world_sql)
+        for id in ids:
+            world = await get_world(get_stmt, id)
+            new_value = randint(1, 10000)
+            updated_worlds.append((new_value, id))
+            worlds.append({'id': id, 'randomNumber': new_value})
+        await update_stmt.executemany(updated_worlds)
+    return worlds
+
+
[email protected]("/plaintext")
+async def test_plaintext(request):
+    return b"Hello, World!"
+
+
+@cached(cache={}, key=lambda stmt, id: hashkey(id))
+async def get_cached_world(stmt, id):
+    result = await stmt.fetchrow(id)
+    return {'id': result[0], 'randomNumber': result[1]}
+
+
[email protected]("/cached-queries")
+async def test_cached_queries(request):
+    async with db.acquire() as conn:
+        get_stmt = await conn.prepare(get_world_sql)
+        worlds = [await get_cached_world(get_stmt, id) for id in generate_ids(get_num_queries(request, 'count'))]
+    return worlds

+ 118 - 0
frameworks/Python/microdot/app_sync.py

@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+from datetime import datetime
+from functools import lru_cache
+import os
+from random import randint
+
+from alchemical import Alchemical
+import sqlalchemy as sqla
+from cachetools import cached
+from cachetools.keys import hashkey
+
+from microdot_wsgi import Microdot
+from microdot_jinja import render_template
+
+app = Microdot()
+db = Alchemical(os.environ['DATABASE_URL'])
+
+
+class World(db.Model):
+    __tablename__ = "world"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    randomnumber = sqla.Column(sqla.Integer)
+
+    def to_dict(self):
+        return {"id": self.id, "randomNumber": self.randomnumber}
+
+
+class CachedWorld(db.Model):
+    __tablename__ = "cachedworld"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    randomnumber = sqla.Column(sqla.Integer)
+
+    def to_dict(self):
+        return {"id": self.id, "randomNumber": self.randomnumber}
+
+
+class Fortune(db.Model):
+    __tablename__ = "fortune"
+    id = sqla.Column(sqla.Integer, primary_key=True)
+    message = sqla.Column(sqla.String)
+
+
+def get_num_queries(request, name="queries"):
+    try:
+        num_queries = request.args.get(name, 1, type=int)
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+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(ids)
+
+
[email protected]("/json")
+def test_json(request):
+    return {"message": "Hello, World!"}
+
+
[email protected]("/db")
+def test_db(request):
+    id = randint(1, 10000)
+    with db.Session() as session:
+        world = session.get(World, id)
+    return world.to_dict()
+
+
[email protected]("/queries")
+def test_queries(request):
+    with db.Session() as session:
+        worlds = [session.get(World, id).to_dict() for id in generate_ids(get_num_queries(request))]
+    return worlds
+
+
[email protected]("/fortunes")
+def test_fortunes(request):
+    with db.Session() as session:
+        fortunes = list(session.scalars(Fortune.select()))
+    fortunes.append(Fortune(id=0, message="Additional fortune added at request time."))
+    fortunes.sort(key=lambda f: f.message)
+    return render_template("fortunes.html", fortunes=fortunes), {'Content-Type': 'text/html; charset=utf-8'}
+
+
[email protected]("/updates")
+def test_updates(request):
+    worlds = []
+    ids = generate_ids(get_num_queries(request))
+    ids.sort()  # to avoid deadlocks
+    with db.begin() as session:
+        for id in ids:
+            world = session.get(World, id)
+            world.randomnumber = (randint(1, 9999) + world.randomnumber - 1) % 10000 + 1
+            worlds.append(world.to_dict())
+    return worlds
+
+
[email protected]("/plaintext")
+def test_plaintext(request):
+    return b"Hello, World!"
+
+
+@cached(cache={}, key=lambda session, id: hashkey(id))
+def get_cached_world(session, id):
+    return session.get(World, id).to_dict()
+
+
[email protected]("/cached-queries")
+def test_cached_queries(request):
+    with db.Session() as session:
+        worlds = [get_cached_world(session, id) for id in generate_ids(get_num_queries(request, 'count'))]
+    return worlds

+ 115 - 0
frameworks/Python/microdot/app_sync_raw.py

@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+from datetime import datetime
+from functools import lru_cache
+import os
+from random import randint
+
+from microdot_wsgi import Microdot
+from microdot_jinja import render_template
+import psycopg2
+from psycopg2.extras import execute_batch
+from cachetools import cached
+from cachetools.keys import hashkey
+
+app = Microdot()
+db = psycopg2.connect(os.environ['DATABASE_URL'])
+
+get_world_sql = 'SELECT id, randomnumber FROM world WHERE id = $1'
+update_world_sql = 'UPDATE world SET randomnumber = $1 WHERE id = $2'
+fortune_sql = 'SELECT * FROM fortune'
+with db.cursor() as cur:
+    cur.execute('PREPARE get_world AS ' + get_world_sql)
+    cur.execute('PREPARE update_world AS ' + update_world_sql)
+    cur.execute('PREPARE fortune AS ' + fortune_sql)
+
+
+def get_num_queries(request, name="queries"):
+    try:
+        num_queries = request.args.get(name, 1, type=int)
+    except ValueError:
+        num_queries = 1
+    if num_queries < 1:
+        return 1
+    if num_queries > 500:
+        return 500
+    return num_queries
+
+
+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(ids)
+
+
[email protected]("/json")
+def test_json(request):
+    return {"message": "Hello, World!"}
+
+
[email protected]("/db")
+def test_db(request):
+    id = randint(1, 10000)
+    with db.cursor() as cur:
+        cur.execute('EXECUTE get_world (%s)', (id,))
+        result = cur.fetchone()
+        world = {'id': result[0], 'randomNumber': result[1]}
+    return world
+
+
+def get_world(cur, id):
+    cur.execute('EXECUTE get_world (%s)', (id,))
+    result = cur.fetchone()
+    return {'id': result[0], 'randomNumber': result[1]}
+
+
[email protected]("/queries")
+def test_queries(request):
+    with db.cursor() as cur:
+        worlds = [get_world(cur, id) for id in generate_ids(get_num_queries(request))]
+    return worlds
+
+
[email protected]("/fortunes")
+def test_fortunes(request):
+    with db.cursor() as cur:
+        cur.execute('EXECUTE fortune')
+        fortunes = list(cur.fetchall())
+    fortunes.append((0, 'Additional fortune added at request time.'))
+    fortunes.sort(key=lambda f: f[1])
+    return render_template("fortunes_raw.html", fortunes=fortunes), {'Content-Type': 'text/html; charset=utf-8'}
+
+
[email protected]("/updates")
+def test_updates(request):
+    worlds = []
+    updated_worlds = []
+    with db.cursor() as cur:
+        for id in generate_ids(get_num_queries(request)):
+            cur.execute('EXECUTE get_world (%s)', (id,))
+            result = cur.fetchone()
+            new_value = randint(1, 10000)
+            updated_worlds.append((new_value, result[0]))
+            worlds.append({'id': result[0], 'randomNumber': new_value})
+        execute_batch(cur, 'EXECUTE update_world (%s, %s)', updated_worlds)
+        db.commit()
+    return worlds
+
+
[email protected]("/plaintext")
+def test_plaintext(request):
+    return b"Hello, World!"
+
+
+@cached(cache={}, key=lambda cur, id: hashkey(id))
+def get_cached_world(cur, id):
+    cur.execute('EXECUTE get_world (%s)', (id,))
+    result = cur.fetchone()
+    return {'id': result[0], 'randomNumber': result[1]}
+
+
[email protected]("/cached-queries")
+def test_cached_queries(request):
+    with db.cursor() as cur:
+        worlds = [get_cached_world(cur, id) for id in generate_ids(get_num_queries(request, 'count'))]
+    return worlds

+ 103 - 0
frameworks/Python/microdot/benchmark_config.json

@@ -0,0 +1,103 @@
+{
+  "framework": "microdot",
+  "tests": [
+    {
+      "default": {
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+	"cached_query_url": "/cached-queries?count=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Microdot",
+        "language": "Python",
+        "flavor": "None",
+        "orm": "Full",
+        "platform": "None",
+        "webserver": "Gunicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Microdot-WSGI",
+        "notes": "",
+        "versus": "None"
+      },
+      "async": {
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+	"cached_query_url": "/cached-queries?count=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Microdot",
+        "language": "Python",
+        "flavor": "None",
+        "orm": "Full",
+        "platform": "None",
+        "webserver": "Uvicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Microdot-ASGI",
+        "notes": "",
+        "versus": "None"
+      },
+      "raw": {
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+	"cached_query_url": "/cached-queries?count=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Microdot",
+        "language": "Python",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "Gunicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Microdot-WSGI-Raw",
+        "notes": "",
+        "versus": "None"
+      },
+      "async-raw": {
+        "json_url": "/json",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "fortune_url": "/fortunes",
+        "update_url": "/updates?queries=",
+        "plaintext_url": "/plaintext",
+	"cached_query_url": "/cached-queries?count=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "Microdot",
+        "language": "Python",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "Uvicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Microdot-ASGI-Raw",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 15 - 0
frameworks/Python/microdot/gunicorn_conf.py

@@ -0,0 +1,15 @@
+import multiprocessing
+import os
+import sys
+
+_is_pypy = hasattr(sys, "pypy_version_info")
+_is_travis = os.environ.get("TRAVIS") == "true"
+
+workers = int(multiprocessing.cpu_count() * 4)
+if _is_travis:
+    workers = 2
+
+bind = "0.0.0.0:8080"
+keepalive = 120
+errorlog = "-"
+pidfile = "gunicorn.pid"

+ 15 - 0
frameworks/Python/microdot/microdot-async-raw.dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.8-buster
+
+RUN apt-get update
+RUN apt-get install libpq-dev python3-dev -y
+ADD ./requirements.txt /microdot/requirements.txt
+RUN pip3 install -r /microdot/requirements.txt
+ADD ./ /microdot
+WORKDIR /microdot
+
+ENV PYTHONUNBUFFERED 1
+ENV DATABASE_URL postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world
+
+EXPOSE 8080
+
+CMD gunicorn app_async_raw:asgi -c uvicorn_conf.py

+ 15 - 0
frameworks/Python/microdot/microdot-async.dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.8-buster
+
+RUN apt-get update
+RUN apt-get install libpq-dev python3-dev -y
+ADD ./requirements.txt /microdot/requirements.txt
+RUN pip3 install -r /microdot/requirements.txt
+ADD ./ /microdot
+WORKDIR /microdot
+
+ENV PYTHONUNBUFFERED 1
+ENV DATABASE_URL postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world
+
+EXPOSE 8080
+
+CMD gunicorn app_async:app -c uvicorn_conf.py

+ 15 - 0
frameworks/Python/microdot/microdot-raw.dockerfile

@@ -0,0 +1,15 @@
+FROM python:3.8-buster
+
+RUN apt-get update
+RUN apt-get install libpq-dev python3-dev -y
+ADD ./requirements.txt /microdot/requirements.txt
+RUN pip3 install -r /microdot/requirements.txt
+ADD ./ /microdot
+WORKDIR /microdot
+
+ENV PYTHONUNBUFFERED 1
+ENV DATABASE_URL "host=tfb-database port=5432 user=benchmarkdbuser password=benchmarkdbpass dbname=hello_world"
+
+EXPOSE 8080
+
+CMD gunicorn app_sync_raw:app -c gunicorn_conf.py

+ 16 - 0
frameworks/Python/microdot/microdot.dockerfile

@@ -0,0 +1,16 @@
+FROM python:3.8-buster
+
+
+RUN apt-get update
+RUN apt-get install libpq-dev python3-dev -y
+ADD ./requirements.txt /microdot/requirements.txt
+RUN pip3 install -r /microdot/requirements.txt
+ADD ./ /microdot
+WORKDIR /microdot
+
+ENV PYTHONUNBUFFERED 1
+ENV DATABASE_URL postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world
+
+EXPOSE 8080
+
+CMD gunicorn app_sync:app -c gunicorn_conf.py

+ 12 - 0
frameworks/Python/microdot/requirements.txt

@@ -0,0 +1,12 @@
+psycopg2-binary
+psycopg2-pool
+asyncpg
+
+jinja2
+cachetools
+asyncache
+
+alchemical
+microdot
+gunicorn
+uvicorn

+ 21 - 0
frameworks/Python/microdot/templates/fortunes.html

@@ -0,0 +1,21 @@
+<!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.id }}</td>
+<td>{{ fortune.message }}</td>
+</tr>
+{% endfor %}
+</table>
+</body>
+</html>
+

+ 21 - 0
frameworks/Python/microdot/templates/fortunes_raw.html

@@ -0,0 +1,21 @@
+<!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] }}</td>
+</tr>
+{% endfor %}
+</table>
+</body>
+</html>
+

+ 15 - 0
frameworks/Python/microdot/uvicorn_conf.py

@@ -0,0 +1,15 @@
+import multiprocessing
+import os
+import sys
+
+_is_travis = os.environ.get("TRAVIS") == "true"
+
+workers = int(multiprocessing.cpu_count())
+if _is_travis:
+    workers = 2
+
+worker_class = 'uvicorn.workers.UvicornWorker'
+bind = "0.0.0.0:8080"
+keepalive = 120
+errorlog = "-"
+pidfile = "gunicorn.pid"