瀏覽代碼

Updates to selected Python frameworks (#7568)

* Use the documented templating system

See https://fastapi.tiangolo.com/advanced/templates/ and
https://www.starlette.io/templates/. This is as a user would
realistically use the frameworks.

* Ensure FastAPI apps differ only by OrJSON usage

This will help maintain these files as now a diff between them only
shows the OrJSON related changes as expected.

* Quart-Hypercorn; match the log level used elsewhere

* Upgrade Quart and requirements

* Update the Starlette and FastAPI dependencies

This includes installing uvicorn[standard] and adding all the FastAPI
dependencies to the pinned requirements list.

* Remove Quart transaction usage

It is not used in other Python frameworks, instead a direct connection
is used.

* Sort fortunes as a user would

lambdas are more common than operator usage (and matches other Python
frameworks).

* Use Starlette's query_params dict

Rather than parsing again in this code.
Phil Jones 2 年之前
父節點
當前提交
b78ceaf563

+ 26 - 38
frameworks/Python/fastapi/app.py

@@ -1,24 +1,14 @@
 import asyncio
 import asyncpg
 import os
-import jinja2
-from fastapi import FastAPI
+from fastapi import FastAPI, Request
+from fastapi.templating import Jinja2Templates
 from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
 from random import randint, sample
-from operator import itemgetter
-from urllib.parse import parse_qs
 
-
-READ_ROW_SQL = 'SELECT "randomnumber", "id" FROM "world" WHERE id = $1'
+READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
 WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
-ADDITIONAL_ROW = [0, 'Additional fortune added at request time.']
-
-
-def load_fortunes_template():
-    path = os.path.join('templates', 'fortune.html')
-    with open(path, 'r') as template_file:
-        template_text = template_file.read()
-        return jinja2.Template(template_text)
+ADDITIONAL_ROW = [0, "Additional fortune added at request time."]
 
 
 def get_num_queries(queries):
@@ -35,40 +25,39 @@ def get_num_queries(queries):
 
 
 connection_pool = None
-sort_fortunes_key = itemgetter(1)
-template = load_fortunes_template()
 
 app = FastAPI()
 
+templates = Jinja2Templates(directory="templates")
+
 
 @app.on_event("startup")
 async def setup_database():
     global connection_pool
     connection_pool = await asyncpg.create_pool(
-        user=os.getenv('PGUSER', 'benchmarkdbuser'),
-        password=os.getenv('PGPASS', 'benchmarkdbpass'),
-        database='hello_world',
-        host='tfb-database',
-        port=5432
+        user=os.getenv("PGUSER", "benchmarkdbuser"),
+        password=os.getenv("PGPASS", "benchmarkdbpass"),
+        database="hello_world",
+        host="tfb-database",
+        port=5432,
     )
 
 
[email protected]('/json')
[email protected]("/json")
 async def json_serialization():
-    return JSONResponse({'message': 'Hello, world!'})
+    return JSONResponse({"message": "Hello, world!"})
 
 
[email protected]('/db')
[email protected]("/db")
 async def single_database_query():
     row_id = randint(1, 10000)
-
     async with connection_pool.acquire() as connection:
         number = await connection.fetchval(READ_ROW_SQL, row_id)
 
-    return JSONResponse({'id': row_id, 'randomNumber': number})
+    return JSONResponse({"id": row_id, "randomNumber": number})
 
 
[email protected]('/queries')
[email protected]("/queries")
 async def multiple_database_queries(queries = None):
     num_queries = get_num_queries(queries)
     row_ids = sample(range(1, 10000), num_queries)
@@ -78,27 +67,26 @@ async def multiple_database_queries(queries = None):
         statement = await connection.prepare(READ_ROW_SQL)
         for row_id in row_ids:
             number = await statement.fetchval(row_id)
-            worlds.append({'id': row_id, 'randomNumber': number})
+            worlds.append({"id": row_id, "randomNumber": number})
 
     return JSONResponse(worlds)
 
 
[email protected]('/fortunes')
-async def fortunes():
[email protected]("/fortunes")
+async def fortunes(request: Request):
     async with connection_pool.acquire() as connection:
-        fortunes = await connection.fetch('SELECT * FROM Fortune')
+        fortunes = await connection.fetch("SELECT * FROM Fortune")
 
     fortunes.append(ADDITIONAL_ROW)
-    fortunes.sort(key=sort_fortunes_key)
-    content = template.render(fortunes=fortunes)
-    return HTMLResponse(content)
+    fortunes.sort(key=lambda row: row[1])
+    return templates.TemplateResponse("fortune.html", {"fortunes": fortunes, "request": request})
 
 
[email protected]('/updates')
[email protected]("/updates")
 async def database_updates(queries = None):
     num_queries = get_num_queries(queries)
     updates = [(row_id, randint(1, 10000)) for row_id in sample(range(1, 10000), num_queries)]
-    worlds = [{'id': row_id, 'randomNumber': number} for row_id, number in updates]
+    worlds = [{"id": row_id, "randomNumber": number} for row_id, number in updates]
 
     async with connection_pool.acquire() as connection:
         statement = await connection.prepare(READ_ROW_SQL)
@@ -109,6 +97,6 @@ async def database_updates(queries = None):
     return JSONResponse(worlds)
 
 
[email protected]('/plaintext')
[email protected]("/plaintext")
 async def plaintext():
-    return PlainTextResponse(b'Hello, world!')
+    return PlainTextResponse(b"Hello, world!")

+ 25 - 36
frameworks/Python/fastapi/app_orjson.py

@@ -1,36 +1,16 @@
 import asyncio
 import asyncpg
 import os
-import jinja2
-from fastapi import FastAPI
+from fastapi import FastAPI, Request
 from fastapi.responses import HTMLResponse, ORJSONResponse, PlainTextResponse
+from fastapi.templating import Jinja2Templates
 from random import randint, sample
-from operator import itemgetter
-from functools import partial
 
 READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
 WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
 ADDITIONAL_ROW = [0, "Additional fortune added at request time."]
 
 
-async def setup_database():
-    global connection_pool
-    connection_pool = await asyncpg.create_pool(
-        user=os.getenv("PGUSER", "benchmarkdbuser"),
-        password=os.getenv("PGPASS", "benchmarkdbpass"),
-        database="hello_world",
-        host="tfb-database",
-        port=5432,
-    )
-
-
-def load_fortunes_template():
-    path = os.path.join("templates", "fortune.html")
-    with open(path, "r") as template_file:
-        template_text = template_file.read()
-        return jinja2.Template(template_text)
-
-
 def get_num_queries(queries):
     try:
         query_count = int(queries)
@@ -45,14 +25,23 @@ def get_num_queries(queries):
 
 
 connection_pool = None
-sort_fortunes_key = itemgetter(1)
-template = load_fortunes_template()
-loop = asyncio.get_event_loop()
-loop.run_until_complete(setup_database())
-
 
 app = FastAPI()
 
+templates = Jinja2Templates(directory="templates")
+
+
[email protected]_event("startup")
+async def setup_database():
+    global connection_pool
+    connection_pool = await asyncpg.create_pool(
+        user=os.getenv("PGUSER", "benchmarkdbuser"),
+        password=os.getenv("PGPASS", "benchmarkdbpass"),
+        database="hello_world",
+        host="tfb-database",
+        port=5432,
+    )
+
 
 @app.get("/json")
 async def json_serialization():
@@ -61,14 +50,15 @@ async def json_serialization():
 
 @app.get("/db")
 async def single_database_query():
+    row_id = randint(1, 10000)
     async with connection_pool.acquire() as connection:
-        record = await connection.fetchrow(READ_ROW_SQL, randint(1, 10000))
+        number = await connection.fetchval(READ_ROW_SQL, row_id)
 
-    return ORJSONResponse({"id": record['id'], "randomNumber": record['randomnumber']})
+    return ORJSONResponse({"id": row_id, "randomNumber": number})
 
 
 @app.get("/queries")
-async def multiple_database_queries(queries=None):
+async def multiple_database_queries(queries = None):
     num_queries = get_num_queries(queries)
     row_ids = sample(range(1, 10000), num_queries)
     worlds = []
@@ -77,24 +67,23 @@ async def multiple_database_queries(queries=None):
         statement = await connection.prepare(READ_ROW_SQL)
         for row_id in row_ids:
             number = await statement.fetchval(row_id)
-            worlds.append({'id': row_id, 'randomNumber': number})
+            worlds.append({"id": row_id, "randomNumber": number})
 
     return ORJSONResponse(worlds)
 
 
 @app.get("/fortunes")
-async def fortunes():
+async def fortunes(request: Request):
     async with connection_pool.acquire() as connection:
         fortunes = await connection.fetch("SELECT * FROM Fortune")
 
     fortunes.append(ADDITIONAL_ROW)
-    fortunes.sort(key=sort_fortunes_key)
-    content = template.render(fortunes=fortunes)
-    return HTMLResponse(content)
+    fortunes.sort(key=lambda row: row[1])
+    return templates.TemplateResponse("fortune.html", {"fortunes": fortunes, "request": request})
 
 
 @app.get("/updates")
-async def database_updates(queries=None):
+async def database_updates(queries = None):
     num_queries = get_num_queries(queries)
     updates = [(row_id, randint(1, 10000)) for row_id in sample(range(1, 10000), num_queries)]
     worlds = [{"id": row_id, "randomNumber": number} for row_id, number in updates]

+ 2 - 2
frameworks/Python/fastapi/fastapi-orjson.dockerfile

@@ -1,10 +1,10 @@
-FROM python:3.8
+FROM python:3.10
 
 ADD ./ /fastapi
 
 WORKDIR /fastapi
 
-RUN pip3 install -r /fastapi/requirements-orjson.txt
+RUN pip install -r /fastapi/requirements-orjson.txt
 
 EXPOSE 8080
 

+ 2 - 3
frameworks/Python/fastapi/fastapi.dockerfile

@@ -1,11 +1,10 @@
-FROM python:3.8
+FROM python:3.10
 
 ADD ./ /fastapi
 
 WORKDIR /fastapi
 
-RUN pip3 install cython==0.29.13 && \
-    pip3 install -r /fastapi/requirements.txt
+RUN pip install -r /fastapi/requirements.txt
 
 EXPOSE 8080
 

+ 21 - 7
frameworks/Python/fastapi/requirements-orjson.txt

@@ -1,7 +1,21 @@
-asyncpg==0.21.0
-gunicorn==20.0.4
-Jinja2==2.11.3
-markupsafe==2.0.1
-fastapi==0.65.2
-orjson==2.6.5
-uvicorn==0.11.3
+anyio==3.6.1
+asyncpg==0.26.0
+click==8.1.3
+fastapi==0.81.0
+gunicorn==20.1.0
+h11==0.13.0
+httptools==0.4.0
+idna==3.3
+Jinja2==3.1.2
+MarkupSafe==2.1.1
+orjson==3.8.0
+pydantic==1.10.1
+python-dotenv==0.20.0
+PyYAML==6.0
+sniffio==1.3.0
+starlette==0.19.1
+typing_extensions==4.3.0
+uvicorn==0.18.3
+uvloop==0.16.0
+watchfiles==0.16.1
+websockets==10.3

+ 20 - 8
frameworks/Python/fastapi/requirements.txt

@@ -1,8 +1,20 @@
-asyncpg==0.21.0
-gunicorn==20.0.4
-Jinja2==2.11.3
-markupsafe==2.0.1
-ujson==5.4.0
-uvloop==0.14.0
-uvicorn==0.11.3
-fastapi==0.65.2
+anyio==3.6.1
+asyncpg==0.26.0
+click==8.1.3
+fastapi==0.81.0
+gunicorn==20.1.0
+h11==0.13.0
+httptools==0.4.0
+idna==3.3
+Jinja2==3.1.2
+MarkupSafe==2.1.1
+pydantic==1.10.1
+python-dotenv==0.20.0
+PyYAML==6.0
+sniffio==1.3.0
+starlette==0.19.1
+typing_extensions==4.3.0
+uvicorn==0.18.3
+uvloop==0.16.0
+watchfiles==0.16.1
+websockets==10.3

+ 6 - 12
frameworks/Python/quart/app.py

@@ -3,7 +3,7 @@ import random
 import os
 
 import asyncpg
-from quart import jsonify, Quart, request, render_template
+from quart import Quart, request, render_template
 
 app = Quart(__name__)
 
@@ -39,10 +39,10 @@ async def plaintext():
 
 @app.get("/db")
 async def db():
+    key = random.randint(1, 10000)
     async with app.db.acquire() as conn:
-        key = random.randint(1, 10000)
         number = await conn.fetchval(GET_WORLD, key)
-        return jsonify({"id": key, "randomNumber": number})
+    return {"id": key, "randomNumber": number}
 
 
 def get_query_count():
@@ -68,7 +68,7 @@ async def queries():
             number = await pst.fetchval(key)
             worlds.append({"id": key, "randomNumber": number})
 
-    return jsonify(worlds)
+    return worlds
 
 
 @app.get("/updates")
@@ -76,7 +76,7 @@ async def updates():
     queries = get_query_count()
 
     new_worlds = []
-    async with app.db.acquire() as conn, conn.transaction():
+    async with app.db.acquire() as conn:
         pst = await conn.prepare(GET_WORLD)
 
         for key in random.sample(range(1, 10000), queries):
@@ -86,9 +86,7 @@ async def updates():
 
         await conn.executemany(UPDATE_WORLD, new_worlds)
 
-    return jsonify(
-        [{"id": key, "randomNumber": new_number} for key, new_number in new_worlds]
-    )
+    return [{"id": key, "randomNumber": new_number} for key, new_number in new_worlds]
 
 
 @app.get("/fortunes")
@@ -99,7 +97,3 @@ async def fortunes():
     rows.sort(key=lambda row: row[1])
 
     return await render_template("fortunes.html", fortunes=rows)
-
-
-if __name__ == "__main__":
-    app.run(host="0.0.0.0", port=8080)

+ 1 - 0
frameworks/Python/quart/hypercorn_conf.py

@@ -9,4 +9,5 @@ if _is_travis:
 
 bind = ["0.0.0.0:8080"]
 keep_alive_timeout = 120
+loglevel = "error"
 worker_class = "uvloop"

+ 1 - 1
frameworks/Python/quart/quart.dockerfile

@@ -4,7 +4,7 @@ ADD ./ /quart
 
 WORKDIR /quart
 
-RUN pip3 install -r /quart/requirements.txt
+RUN pip install -r /quart/requirements.txt
 
 EXPOSE 8080
 

+ 2 - 2
frameworks/Python/quart/requirements-uvicorn.txt

@@ -4,7 +4,7 @@ httptools==0.4.0
 idna==3.3
 python-dotenv==0.20.0
 PyYAML==6.0
-sniffio==1.2.0
-uvicorn==0.18.2
+sniffio==1.3.0
+uvicorn==0.18.3
 watchfiles==0.16.1
 websockets==10.3

+ 3 - 3
frameworks/Python/quart/requirements.txt

@@ -5,7 +5,7 @@ click==8.1.3
 h11==0.13.0
 h2==4.1.0
 hpack==4.0.0
-hypercorn==0.13.2
+hypercorn==0.14.2
 hyperframe==6.0.1
 itsdangerous==2.1.2
 Jinja2==3.1.2
@@ -14,5 +14,5 @@ priority==2.0.0
 quart==0.18.0
 toml==0.10.2
 uvloop==0.16.0
-Werkzeug==2.2.0
-wsproto==1.1.0
+Werkzeug==2.2.2
+wsproto==1.2.0

+ 5 - 17
frameworks/Python/starlette/app.py

@@ -1,12 +1,10 @@
 import asyncpg
 import os
-import jinja2
 from starlette.applications import Starlette
 from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
 from starlette.routing import Route
+from starlette.templating import Jinja2Templates
 from random import randint, sample
-from operator import itemgetter
-from urllib.parse import parse_qs
 
 
 READ_ROW_SQL = 'SELECT "randomnumber", "id" FROM "world" WHERE id = $1'
@@ -26,17 +24,9 @@ async def setup_database():
     )
 
 
-def load_fortunes_template():
-    path = os.path.join('templates', 'fortune.html')
-    with open(path, 'r') as template_file:
-        template_text = template_file.read()
-        return jinja2.Template(template_text)
-
-
 def get_num_queries(request):
     try:
-        query_string = request['query_string']
-        query_count = int(parse_qs(query_string)[b'queries'][0])
+        query_count = int(request.query_params["queries"])
     except (KeyError, IndexError, ValueError):
         return 1
 
@@ -48,8 +38,7 @@ def get_num_queries(request):
 
 
 connection_pool = None
-sort_fortunes_key = itemgetter(1)
-template = load_fortunes_template()
+templates = Jinja2Templates(directory="templates")
 
 
 async def single_database_query(request):
@@ -80,9 +69,8 @@ async def fortunes(request):
         fortunes = await connection.fetch('SELECT * FROM Fortune')
 
     fortunes.append(ADDITIONAL_ROW)
-    fortunes.sort(key=sort_fortunes_key)
-    content = template.render(fortunes=fortunes)
-    return HTMLResponse(content)
+    fortunes.sort(key=lambda row: row[1])
+    return templates.TemplateResponse("fortune.html", {"fortunes": fortunes, "request": request})
 
 
 async def database_updates(request):

+ 14 - 4
frameworks/Python/starlette/requirements.txt

@@ -1,7 +1,17 @@
-asyncpg==0.25.0
+anyio==3.6.1
+asyncpg==0.26.0
+click==8.1.3
 gunicorn==20.1.0
+h11==0.13.0
+httptools==0.4.0
+idna==3.3
 Jinja2==3.1.2
-starlette==0.19.1
-ujson==5.4.0
-uvicorn==0.17.6
+MarkupSafe==2.1.1
+python-dotenv==0.20.0
+PyYAML==6.0
+sniffio==1.3.0
+starlette==0.20.4
+uvicorn==0.18.3
 uvloop==0.16.0
+watchfiles==0.16.1
+websockets==10.3

+ 2 - 3
frameworks/Python/starlette/starlette.dockerfile

@@ -1,11 +1,10 @@
-FROM python:3.8
+FROM python:3.10
 
 ADD ./ /starlette
 
 WORKDIR /starlette
 
-RUN pip3 install cython==0.29.13 && \
-    pip3 install -r /starlette/requirements.txt
+RUN pip install -r /starlette/requirements.txt
 
 EXPOSE 8080