Переглянути джерело

[PYTHON][FASTAPI] Upgrades & improvements (#7613)

* [PYTHON][FASTAPI] Upgrades & improvements

Update to Python 3.9
Update all dependencies

Add nginx-unit
Add uvicorn
Add hypercorn

Add SQLAlchemy ORM

* [FASTAPI] Disable nginx-unit access-log
Micael Malta 2 роки тому
батько
коміт
413877bf88
26 змінених файлів з 721 додано та 189 видалено
  1. 34 14
      frameworks/Python/fastapi/app.py
  2. 0 102
      frameworks/Python/fastapi/app_orjson.py
  3. 157 0
      frameworks/Python/fastapi/app_orm.py
  4. 167 7
      frameworks/Python/fastapi/benchmark_config.json
  5. 126 4
      frameworks/Python/fastapi/config.toml
  6. 18 0
      frameworks/Python/fastapi/fastapi-gunicorn-orjson.dockerfile
  7. 20 0
      frameworks/Python/fastapi/fastapi-gunicorn-orm.dockerfile
  8. 18 0
      frameworks/Python/fastapi/fastapi-gunicorn.dockerfile
  9. 18 0
      frameworks/Python/fastapi/fastapi-hypercorn-orjson.dockerfile
  10. 18 0
      frameworks/Python/fastapi/fastapi-hypercorn.dockerfile
  11. 20 0
      frameworks/Python/fastapi/fastapi-nginx-unit-orjson.dockerfile
  12. 20 0
      frameworks/Python/fastapi/fastapi-nginx-unit.dockerfile
  13. 0 11
      frameworks/Python/fastapi/fastapi-orjson.dockerfile
  14. 18 0
      frameworks/Python/fastapi/fastapi-uvicorn-orjson.dockerfile
  15. 18 0
      frameworks/Python/fastapi/fastapi-uvicorn.dockerfile
  16. 0 11
      frameworks/Python/fastapi/fastapi.dockerfile
  17. 0 2
      frameworks/Python/fastapi/fastapi_conf.py
  18. 26 0
      frameworks/Python/fastapi/nginx-unit-config-orjson.sh
  19. 26 0
      frameworks/Python/fastapi/nginx-unit-config.sh
  20. 1 0
      frameworks/Python/fastapi/requirements-gunicorn.txt
  21. 1 0
      frameworks/Python/fastapi/requirements-hypercorn.txt
  22. 0 20
      frameworks/Python/fastapi/requirements-orjson.txt
  23. 2 0
      frameworks/Python/fastapi/requirements-sqlalchemy.txt
  24. 1 0
      frameworks/Python/fastapi/requirements-uvicorn.txt
  25. 2 18
      frameworks/Python/fastapi/requirements.txt
  26. 10 0
      frameworks/Python/fastapi/templates/fortune.jinja

+ 34 - 14
frameworks/Python/fastapi/app.py

@@ -1,9 +1,15 @@
-import asyncio
 import asyncpg
 import os
 from fastapi import FastAPI, Request
+from fastapi.responses import PlainTextResponse
+
+try:
+    import orjson
+    from fastapi.responses import ORJSONResponse as JSONResponse
+except ImportError:
+    from fastapi.responses import UJSONResponse as JSONResponse
+
 from fastapi.templating import Jinja2Templates
-from starlette.responses import HTMLResponse, JSONResponse, PlainTextResponse
 from random import randint, sample
 
 READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
@@ -26,15 +32,13 @@ def get_num_queries(queries):
 
 connection_pool = None
 
-app = FastAPI()
-
 templates = Jinja2Templates(directory="templates")
 
+app = FastAPI()
+
 
[email protected]_event("startup")
 async def setup_database():
-    global connection_pool
-    connection_pool = await asyncpg.create_pool(
+    return await asyncpg.create_pool(
         user=os.getenv("PGUSER", "benchmarkdbuser"),
         password=os.getenv("PGPASS", "benchmarkdbpass"),
         database="hello_world",
@@ -43,6 +47,16 @@ async def setup_database():
     )
 
 
[email protected]_event("startup")
+async def startup_event():
+    app.state.connection_pool = await setup_database()
+
+
[email protected]_event("shutdown")
+async def shutdown_event():
+    await app.state.connection_pool.close()
+
+
 @app.get("/json")
 async def json_serialization():
     return JSONResponse({"message": "Hello, world!"})
@@ -51,7 +65,7 @@ async def json_serialization():
 @app.get("/db")
 async def single_database_query():
     row_id = randint(1, 10000)
-    async with connection_pool.acquire() as connection:
+    async with app.state.connection_pool.acquire() as connection:
         number = await connection.fetchval(READ_ROW_SQL, row_id)
 
     return JSONResponse({"id": row_id, "randomNumber": number})
@@ -63,7 +77,7 @@ async def multiple_database_queries(queries = None):
     row_ids = sample(range(1, 10000), num_queries)
     worlds = []
 
-    async with connection_pool.acquire() as connection:
+    async with app.state.connection_pool.acquire() as connection:
         statement = await connection.prepare(READ_ROW_SQL)
         for row_id in row_ids:
             number = await statement.fetchval(row_id)
@@ -74,7 +88,7 @@ async def multiple_database_queries(queries = None):
 
 @app.get("/fortunes")
 async def fortunes(request: Request):
-    async with connection_pool.acquire() as connection:
+    async with app.state.connection_pool.acquire() as connection:
         fortunes = await connection.fetch("SELECT * FROM Fortune")
 
     fortunes.append(ADDITIONAL_ROW)
@@ -85,12 +99,18 @@ async def fortunes(request: Request):
 @app.get("/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]
+    # To avoid deadlock
+    ids = sorted(sample(range(1, 10000 + 1), num_queries))
+    numbers = sorted(sample(range(1, 10000), num_queries))
+    updates = list(zip(ids, numbers))
+
+    worlds = [
+        {"id": row_id, "randomNumber": number} for row_id, number in updates
+    ]
 
-    async with connection_pool.acquire() as connection:
+    async with app.state.connection_pool.acquire() as connection:
         statement = await connection.prepare(READ_ROW_SQL)
-        for row_id, number in updates:
+        for row_id, _ in updates:
             await statement.fetchval(row_id)
         await connection.executemany(WRITE_ROW_SQL, updates)
 

+ 0 - 102
frameworks/Python/fastapi/app_orjson.py

@@ -1,102 +0,0 @@
-import asyncio
-import asyncpg
-import os
-from fastapi import FastAPI, Request
-from fastapi.responses import HTMLResponse, ORJSONResponse, PlainTextResponse
-from fastapi.templating import Jinja2Templates
-from random import randint, sample
-
-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 get_num_queries(queries):
-    try:
-        query_count = int(queries)
-    except (ValueError, TypeError):
-        return 1
-
-    if query_count < 1:
-        return 1
-    if query_count > 500:
-        return 500
-    return query_count
-
-
-connection_pool = None
-
-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,
-    )
-
-
[email protected]("/json")
-async def json_serialization():
-    return ORJSONResponse({"message": "Hello, world!"})
-
-
[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 ORJSONResponse({"id": row_id, "randomNumber": number})
-
-
[email protected]("/queries")
-async def multiple_database_queries(queries = None):
-    num_queries = get_num_queries(queries)
-    row_ids = sample(range(1, 10000), num_queries)
-    worlds = []
-
-    async with connection_pool.acquire() as connection:
-        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})
-
-    return ORJSONResponse(worlds)
-
-
[email protected]("/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=lambda row: row[1])
-    return templates.TemplateResponse("fortune.html", {"fortunes": fortunes, "request": request})
-
-
[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]
-
-    async with connection_pool.acquire() as connection:
-        statement = await connection.prepare(READ_ROW_SQL)
-        for row_id, number in updates:
-            await statement.fetchval(row_id)
-        await connection.executemany(WRITE_ROW_SQL, updates)
-
-    return ORJSONResponse(worlds)
-
-
[email protected]("/plaintext")
-async def plaintext():
-    return PlainTextResponse(b"Hello, world!")

+ 157 - 0
frameworks/Python/fastapi/app_orm.py

@@ -0,0 +1,157 @@
+import logging
+import multiprocessing
+import os
+from operator import attrgetter
+from random import randint, sample
+
+from sqlalchemy import Column, Integer, String, select
+from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm.attributes import flag_modified
+
+from fastapi import FastAPI, Request
+from fastapi.responses import PlainTextResponse, UJSONResponse
+from fastapi.templating import Jinja2Templates
+
+logger = logging.getLogger(__name__)
+
+Base = declarative_base()
+
+
+class World(Base):
+    __tablename__ = "world"
+    id = Column(Integer, primary_key=True)
+    randomnumber = Column(Integer)
+
+    def __json__(self):
+        return {"id": self.id, "randomnumber": self.randomnumber}
+
+
+sa_data = World.__table__
+
+
+class Fortune(Base):
+    __tablename__ = "fortune"
+    id = Column(Integer, primary_key=True)
+    message = Column(String)
+
+
+sa_fortunes = Fortune.__table__
+
+ADDITIONAL_FORTUNE = Fortune(
+    id=0, message="Additional fortune added at request time."
+)
+
+sort_fortunes_key = attrgetter("message")
+
+template_path = os.path.join(
+    os.path.dirname(os.path.realpath(__file__)), "templates"
+)
+templates = Jinja2Templates(directory=template_path)
+
+app = FastAPI()
+
+
+async def setup_database():
+    dsn = "postgresql+asyncpg://%s:%s@tfb-database:5432/hello_world" % (
+        os.getenv("PGPASS", "benchmarkdbuser"),
+        os.getenv("PGPASS", "benchmarkdbpass"),
+    )
+
+    engine = create_async_engine(
+        dsn,
+        future=True,
+        connect_args={
+            "ssl": False  # NEEDED FOR NGINX-UNIT OTHERWISE IT FAILS
+        },
+    )
+    return sessionmaker(engine, class_=AsyncSession)
+
+
+def get_num_queries(queries):
+    try:
+        query_count = int(queries)
+    except (ValueError, TypeError):
+        return 1
+
+    if query_count < 1:
+        return 1
+    if query_count > 500:
+        return 500
+    return query_count
+
+
[email protected]_event("startup")
+async def startup_event():
+    app.state.db_session = await setup_database()
+
+
[email protected]_event("shutdown")
+async def shutdown_event():
+    await app.state.db_session.close()
+
+
[email protected]("/json")
+async def json_serialization():
+    return UJSONResponse({"message": "Hello, world!"})
+
+
[email protected]("/db")
+async def single_database_query():
+    id_ = randint(1, 10000)
+
+    async with app.state.db_session() as sess:
+        result = await sess.get(World, id_)
+
+    return UJSONResponse(result.__json__())
+
+
[email protected]("/queries")
+async def multiple_database_queries(queries=None):
+    num_queries = get_num_queries(queries)
+    data = []
+
+    async with app.state.db_session() as sess:
+        for id_ in sample(range(1, 10001), num_queries):
+            result = await sess.get(World, id_)
+            data.append(result.__json__())
+
+    return UJSONResponse(data)
+
+
[email protected]("/fortunes")
+async def fortunes(request: Request):
+    async with app.state.db_session() as sess:
+        ret = await sess.execute(select(Fortune.id, Fortune.message))
+        data = ret.all()
+
+    data.append(ADDITIONAL_FORTUNE)
+    data.sort(key=sort_fortunes_key)
+
+    return templates.TemplateResponse(
+        "fortune.jinja", {"request": request, "fortunes": data}
+    )
+
+
[email protected]("/updates")
+async def database_updates(queries=None):
+    num_queries = get_num_queries(queries)
+
+    ids = sorted(sample(range(1, 10000 + 1), num_queries))
+    data = []
+    async with app.state.db_session.begin() as sess:
+        for id_ in ids:
+            world = await sess.get(World, id_, populate_existing=True)
+            world.randomnumber = randint(1, 10000)
+            # force sqlalchemy to UPDATE entry even if the value has not changed
+            # doesn't make sense in a real application, added only for pass `tfb verify`
+            flag_modified(world, "randomnumber")
+            data.append(world.__json__())
+
+    return UJSONResponse(data)
+
+
[email protected]("/plaintext")
+async def plaintext():
+    return PlainTextResponse(b"Hello, world!")

+ 167 - 7
frameworks/Python/fastapi/benchmark_config.json

@@ -2,7 +2,7 @@
   "framework": "fastapi",
   "tests": [
     {
-      "default": {
+      "gunicorn": {
         "json_url": "/json",
         "fortune_url": "/fortunes",
         "plaintext_url": "/plaintext",
@@ -17,15 +17,15 @@
         "language": "Python",
         "flavor": "Python3",
         "orm": "Raw",
-        "platform": "None",
-        "webserver": "None",
+        "platform": "asyncio",
+        "webserver": "Gunicorn",
         "os": "Linux",
         "database_os": "Linux",
         "display_name": "FastAPI",
         "notes": "",
         "versus": "None"
       },
-      "orjson": {
+      "gunicorn-orjson": {
         "json_url": "/json",
         "fortune_url": "/fortunes",
         "plaintext_url": "/plaintext",
@@ -40,11 +40,171 @@
         "language": "Python",
         "flavor": "Python3",
         "orm": "Raw",
-        "platform": "None",
-        "webserver": "None",
+        "platform": "asyncio",
+        "webserver": "Gunicorn",
         "os": "Linux",
         "database_os": "Linux",
-        "display_name": "FastAPI",
+        "display_name": "FastAPI-gunicorn-orjson",
+        "notes": "",
+        "versus": "None"
+      },
+      "gunicorn-orm": {
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Full",
+        "platform": "asyncio",
+        "webserver": "Gunicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-gunicorn-orm",
+        "notes": "",
+        "versus": "None"
+      },
+      "hypercorn": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "Hypercorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-hypercorn",
+        "notes": "",
+        "versus": "None"
+      },
+      "hypercorn-orjson": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "Hypercorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-hypercorn-orjson",
+        "notes": "",
+        "versus": "None"
+      },
+      "nginx-unit": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "nginx-unit",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-nginx-unit",
+        "notes": "",
+        "versus": "None"
+      },
+      "nginx-unit-orjson": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "nginx-unit",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-nginx-unit-orjson",
+        "notes": "",
+        "versus": "None"
+      },
+      "uvicorn": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "Uvicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-uvicorn",
+        "notes": "",
+        "versus": "None"
+      },
+      "uvicorn-orjson": {
+        "json_url": "/json",
+        "fortune_url": "/fortunes",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "FastAPI",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "asyncio",
+        "webserver": "Uvicorn",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "FastAPI-uvicorn-orjson",
         "notes": "",
         "versus": "None"
       }

+ 126 - 4
frameworks/Python/fastapi/config.toml

@@ -1,7 +1,7 @@
 [framework]
 name = "fastapi"
 
-[main]
+[gunicorn]
 urls.plaintext = "/plaintext"
 urls.json = "/json"
 urls.db = "/db"
@@ -15,10 +15,10 @@ database_os = "Linux"
 os = "Linux"
 orm = "Raw"
 platform = "None"
-webserver = "None"
+webserver = "Gunicorn"
 versus = "None"
 
-[orjson]
+[gunicorn-orjson]
 urls.plaintext = "/plaintext"
 urls.json = "/json"
 urls.db = "/db"
@@ -32,5 +32,127 @@ database_os = "Linux"
 os = "Linux"
 orm = "Raw"
 platform = "None"
-webserver = "None"
+webserver = "Gunicorn"
+versus = "None"
+
+
+[gunicorn-orm]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Full"
+platform = "None"
+webserver = "Gunicorn"
+versus = "None"
+
+[hypercorn]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "Hypercorn"
+versus = "None"
+
+
+[hypercorn-orjson]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "Hypercorn"
+versus = "None"
+
+[nginx-unit]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "nginx-unit"
+versus = "None"
+
+[nginx-unit-orjson]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "nginx-unit"
+versus = "None"
+
+
+[uvicorn]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "uvicorn"
+versus = "None"
+
+[uvicorn-orjson]
+urls.plaintext = "/plaintext"
+urls.json = "/json"
+urls.db = "/db"
+urls.query = "/queries?queries="
+urls.update = "/updates?queries="
+urls.fortune = "/fortunes"
+approach = "Realistic"
+classification = "Micro"
+database = "Postgres"
+database_os = "Linux"
+os = "Linux"
+orm = "Raw"
+platform = "None"
+webserver = "uvicorn"
 versus = "None"

+ 18 - 0
frameworks/Python/fastapi/fastapi-gunicorn-orjson.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-orjson.txt requirements-gunicorn.txt requirements-uvicorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-orjson.txt -r requirements-gunicorn.txt -r requirements-uvicorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c fastapi_conf.py

+ 20 - 0
frameworks/Python/fastapi/fastapi-gunicorn-orm.dockerfile

@@ -0,0 +1,20 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-sqlalchemy.txt requirements-gunicorn.txt requirements-uvicorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-sqlalchemy.txt -r requirements-gunicorn.txt -r requirements-uvicorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+ENV CONNECTION=ORM
+
+CMD gunicorn app_orm:app -k uvicorn.workers.UvicornWorker -c fastapi_conf.py

+ 18 - 0
frameworks/Python/fastapi/fastapi-gunicorn.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-gunicorn.txt requirements-uvicorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-gunicorn.txt -r requirements-uvicorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c fastapi_conf.py

+ 18 - 0
frameworks/Python/fastapi/fastapi-hypercorn-orjson.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-orjson.txt requirements-hypercorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-orjson.txt -r requirements-hypercorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD hypercorn app:app --bind 0.0.0.0:8080 --workers $(nproc)

+ 18 - 0
frameworks/Python/fastapi/fastapi-hypercorn.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-hypercorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-hypercorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD hypercorn app:app --bind 0.0.0.0:8080 --workers $(nproc)

+ 20 - 0
frameworks/Python/fastapi/fastapi-nginx-unit-orjson.dockerfile

@@ -0,0 +1,20 @@
+FROM nginx/unit:1.28.0-python3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-orjson.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-orjson.txt
+
+COPY . ./
+
+COPY ./nginx-unit-config-orjson.sh /docker-entrypoint.d/
+
+ENV PGSSLMODE disable
+
+EXPOSE 8080

+ 20 - 0
frameworks/Python/fastapi/fastapi-nginx-unit.dockerfile

@@ -0,0 +1,20 @@
+FROM nginx/unit:1.28.0-python3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt ./
+
+RUN pip3 install -r requirements.txt
+
+COPY . ./
+
+COPY ./nginx-unit-config.sh /docker-entrypoint.d/
+
+ENV PGSSLMODE disable
+
+EXPOSE 8080

+ 0 - 11
frameworks/Python/fastapi/fastapi-orjson.dockerfile

@@ -1,11 +0,0 @@
-FROM python:3.10
-
-ADD ./ /fastapi
-
-WORKDIR /fastapi
-
-RUN pip install -r /fastapi/requirements-orjson.txt
-
-EXPOSE 8080
-
-CMD gunicorn app_orjson:app -k uvicorn.workers.UvicornWorker -c fastapi_conf.py

+ 18 - 0
frameworks/Python/fastapi/fastapi-uvicorn-orjson.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-orjson.txt requirements-uvicorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-orjson.txt -r requirements-uvicorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD uvicorn app:app --host 0.0.0.0 --port 8080 --workers $(nproc) --log-level error

+ 18 - 0
frameworks/Python/fastapi/fastapi-uvicorn.dockerfile

@@ -0,0 +1,18 @@
+FROM python:3.10
+
+WORKDIR /fastapi
+
+RUN python -m venv /opt/venv
+ENV PATH="/opt/venv/bin:$PATH"
+
+RUN pip3 install cython==0.29.32
+
+COPY requirements.txt requirements-uvicorn.txt ./
+
+RUN pip3 install -r requirements.txt -r requirements-uvicorn.txt
+
+COPY . ./
+
+EXPOSE 8080
+
+CMD uvicorn app:app --host 0.0.0.0 --port 8080 --workers $(nproc) --log-level error

+ 0 - 11
frameworks/Python/fastapi/fastapi.dockerfile

@@ -1,11 +0,0 @@
-FROM python:3.10
-
-ADD ./ /fastapi
-
-WORKDIR /fastapi
-
-RUN pip install -r /fastapi/requirements.txt
-
-EXPOSE 8080
-
-CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c fastapi_conf.py

+ 0 - 2
frameworks/Python/fastapi/fastapi_conf.py

@@ -4,8 +4,6 @@ import os
 _is_travis = os.environ.get('TRAVIS') == 'true'
 
 workers = multiprocessing.cpu_count()
-if _is_travis:
-    workers = 2
 
 bind = "0.0.0.0:8080"
 keepalive = 120

+ 26 - 0
frameworks/Python/fastapi/nginx-unit-config-orjson.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+config='{'
+config+='  "listeners": {'
+config+='    "*:8080": {'
+config+='      "pass": "applications/fastapi"'
+config+='    }'
+config+='  },'
+config+='  "applications": {'
+config+='    "fastapi": {'
+config+='      "type": "python",'
+config+='      "path": "/fastapi",'
+config+='      "home": "/opt/venv/",'
+config+='      "protocol": "asgi",'
+config+='      "module": "app",'
+config+='      "callable": "app",'
+config+='      "processes": '"$(nproc)"','
+config+='    }'
+config+='  }',
+config+='  "access_log": "/dev/null"'
+config+='}'
+
+curl -X PUT \
+     --data-binary "$config" \
+     --unix-socket /var/run/control.unit.sock \
+     http://localhost/config

+ 26 - 0
frameworks/Python/fastapi/nginx-unit-config.sh

@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+
+config='{'
+config+='  "listeners": {'
+config+='    "*:8080": {'
+config+='      "pass": "applications/fastapi"'
+config+='    }'
+config+='  },'
+config+='  "applications": {'
+config+='    "fastapi": {'
+config+='      "type": "python",'
+config+='      "path": "/fastapi",'
+config+='      "home": "/opt/venv/",'
+config+='      "protocol": "asgi",'
+config+='      "module": "app",'
+config+='      "callable": "app",'
+config+='      "processes": '"$(nproc)"','
+config+='    }'
+config+='  }',
+config+='  "access_log": "/dev/null"'
+config+='}'
+
+curl -X PUT \
+     --data-binary "$config" \
+     --unix-socket /var/run/control.unit.sock \
+     http://localhost/config

+ 1 - 0
frameworks/Python/fastapi/requirements-gunicorn.txt

@@ -0,0 +1 @@
+gunicorn==20.1.0

+ 1 - 0
frameworks/Python/fastapi/requirements-hypercorn.txt

@@ -0,0 +1 @@
+hypercorn==0.14.3

+ 0 - 20
frameworks/Python/fastapi/requirements-orjson.txt

@@ -1,21 +1 @@
-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

+ 2 - 0
frameworks/Python/fastapi/requirements-sqlalchemy.txt

@@ -0,0 +1,2 @@
+psycopg2==2.9.3
+SQLAlchemy==1.4.41

+ 1 - 0
frameworks/Python/fastapi/requirements-uvicorn.txt

@@ -0,0 +1 @@
+uvicorn==0.18.3

+ 2 - 18
frameworks/Python/fastapi/requirements.txt

@@ -1,20 +1,4 @@
-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
+fastapi==0.85.0
 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
+ujson==5.5.0

+ 10 - 0
frameworks/Python/fastapi/templates/fortune.jinja

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