Browse Source

[Python] Add Xpresso (#7542)

* [Python] Add Xpresso

* fix deps and obvious blunder on processes

* cleanup

* fmt

* revert change

* edit readme

* fixes

* revert changes

* remove pycache
Adrian Garcia Badaracco 3 years ago
parent
commit
b86471dd97

+ 25 - 0
frameworks/Python/xpresso/README.md

@@ -0,0 +1,25 @@
+# Xpresso Benchmarking Test
+
+This is the Xpresso portion of a [benchmarking tests suite](../../) comparing a variety of web development platforms.
+
+The information below is specific to Xpresso.
+For further guidance, review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
+Also note that there is additional information provided in the [Python README](../).
+
+## Description
+
+[Xpresso] is a high performance Python web framework built on top of [Starlette], [Pydantic] and [di] with a focus on composability, flexibility and minimal but well defined library APIs.
+
+## Test Paths & Sources
+
+All of the test implementations are located within a single file ([main.py](main.py)).
+
+## Resources
+
+* [Xpresso source code on GitHub](https://github.com/adriangb/xpresso)
+* [Xpresso website - documentation](https://xpresso-api.dev)
+
+[Xpresso]: https://github.com/adriangb/xpresso
+[Starlette]: https://github.com/encode/starlette
+[Pydantic]: https://github.com/samuelcolvin/pydantic/
+[di]: https://github.com/adriangb/di

+ 30 - 0
frameworks/Python/xpresso/benchmark_config.json

@@ -0,0 +1,30 @@
+{
+  "framework": "xpresso",
+  "tests": [
+    {
+      "default": {
+        "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": "Xpresso",
+        "language": "Python",
+        "flavor": "Python3",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Xpresso",
+        "notes": "",
+        "versus": "None"
+      }
+    }
+  ]
+}

+ 19 - 0
frameworks/Python/xpresso/config.toml

@@ -0,0 +1,19 @@
+[framework]
+name = "xpresso"
+
+[main]
+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 = "None"
+versus = "None"

+ 158 - 0
frameworks/Python/xpresso/main.py

@@ -0,0 +1,158 @@
+import multiprocessing
+import os
+import pathlib
+from operator import itemgetter
+from random import randint, sample
+from typing import Annotated, AsyncIterable, Optional
+
+import asyncpg  # type: ignore
+import jinja2  # type: ignore
+import uvicorn  # type: ignore
+from pydantic import BaseModel, Field
+from starlette.responses import HTMLResponse, PlainTextResponse
+from xpresso import App, Depends, Path, Response, FromQuery
+
+READ_ROW_SQL = 'SELECT "randomnumber", "id" 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.')
+
+
+sort_fortunes_key = itemgetter(1)
+
+app_dir = pathlib.Path(__file__).parent
+with (app_dir / "templates" / "fortune.html").open() as template_file:
+    template = jinja2.Template(template_file.read())
+
+
+async def get_db_pool() -> AsyncIterable[asyncpg.Pool]:
+    async with asyncpg.create_pool(  # type: ignore
+        user=os.getenv('PGUSER', 'benchmarkdbuser'),
+        password=os.getenv('PGPASS', 'benchmarkdbpass'),
+        database=os.getenv('PGDB', 'hello_world'),
+        host=os.getenv('PGHOST', 'tfb-database'),
+        port=5432,
+    ) as pool:
+        yield pool
+
+
+DBPool = Annotated[asyncpg.Pool, Depends(get_db_pool, scope="app")]
+
+
+def get_num_queries(queries: Optional[str]) -> int:
+    if not queries:
+        return 1
+    try:
+        queries_num = int(queries)
+    except (ValueError, TypeError):
+        return 1
+    if queries_num < 1:
+        return 1
+    if queries_num > 500:
+        return 500
+    return queries_num
+
+
+
+class Greeting(BaseModel):
+    message: str
+
+
+def json_serialization() -> Greeting:
+    return Greeting(message="Hello, world!")
+
+
+def plaintext() -> Response:
+    return PlainTextResponse(b"Hello, world!")
+
+
+class QueryResult(BaseModel):
+    id: int
+    randomNumber: int
+
+
+async def single_database_query(pool: DBPool) -> QueryResult:
+    row_id = randint(1, 10000)
+
+    connection: "asyncpg.Connection"
+    async with pool.acquire() as connection:  # type: ignore
+        number: int = await connection.fetchval(READ_ROW_SQL, row_id)  # type: ignore
+
+    return QueryResult.construct(id=row_id, randomNumber=number)
+
+
+QueryCount = Annotated[str, Field(gt=0, le=500)]
+
+
+async def multiple_database_queries(
+    pool: DBPool,
+    queries: FromQuery[str | None] = None,
+) -> list[QueryResult]:
+    num_queries = get_num_queries(queries)
+    row_ids = sample(range(1, 10000), num_queries)
+
+    connection: "asyncpg.Connection"
+    async with pool.acquire() as connection:  # type: ignore
+        statement = await connection.prepare(READ_ROW_SQL)  # type: ignore
+        return [
+            QueryResult.construct(
+                id=row_id,
+                randomNumber=await statement.fetchval(row_id),  # type: ignore
+            )
+            for row_id in row_ids
+        ]
+
+
+async def fortunes(pool: DBPool) -> Response:
+    connection: "asyncpg.Connection"
+    async with pool.acquire() as connection:  # type: ignore
+        fortunes: "list[tuple[int, str]]" = await connection.fetch("SELECT * FROM Fortune")  # type: ignore
+
+    fortunes.append(ADDITIONAL_ROW)
+    fortunes.sort(key=sort_fortunes_key)
+    content = template.render(fortunes=fortunes)  # type: ignore
+    return HTMLResponse(content)
+
+
+async def database_updates(
+    pool: DBPool,
+    queries: FromQuery[str | None] = None,
+) -> list[QueryResult]:
+    num_queries = get_num_queries(queries)
+
+    updates = [(row_id, randint(1, 10000)) for row_id in sample(range(1, 10000), num_queries)]
+
+    async with pool.acquire() as connection:
+        statement = await connection.prepare(READ_ROW_SQL)
+        for row_id, _ in updates:
+            await statement.fetchval(row_id)
+        await connection.executemany(WRITE_ROW_SQL, updates)  # type: ignore
+
+    return [QueryResult.construct(id=row_id, randomNumber=number) for row_id, number in updates]
+
+
+routes = (
+    Path("/json", get=json_serialization),
+    Path("/plaintext", get=plaintext),
+    Path("/db", get=single_database_query),
+    Path("/queries", get=multiple_database_queries),
+    Path("/fortunes", get=fortunes),
+    Path("/updates", get=database_updates),
+)
+
+
+app = App(routes=routes)
+
+
+if __name__ == "__main__":
+    workers = multiprocessing.cpu_count()
+    if os.environ.get("TRAVIS") == "true":
+        workers = 2
+    uvicorn.run(  # type: ignore
+        "main:app",
+        host="0.0.0.0",
+        port=8080,
+        workers=workers,
+        log_level="error",
+        loop="uvloop",
+        http="httptools",
+    )

+ 7 - 0
frameworks/Python/xpresso/requirements.txt

@@ -0,0 +1,7 @@
+asyncpg==0.26.0
+Jinja2==2.11.3
+markupsafe==2.0.1
+httptools==0.4.0
+uvloop==0.16.0
+uvicorn==0.18.2
+xpresso==0.42.0

+ 10 - 0
frameworks/Python/xpresso/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>

+ 14 - 0
frameworks/Python/xpresso/xpresso.dockerfile

@@ -0,0 +1,14 @@
+FROM python:3.10
+
+RUN mkdir /xpresso
+WORKDIR /xpresso
+
+COPY ./requirements.txt /xpresso/
+
+RUN pip install --no-cache-dir -r /xpresso/requirements.txt
+
+COPY ./ /xpresso/
+
+EXPOSE 8080
+
+CMD ["python", "main.py"]