Skip to content

Commit 4dfb9e0

Browse files
authored
Add Panther (#8636)
1 parent 2638fd9 commit 4dfb9e0

File tree

8 files changed

+266
-0
lines changed

8 files changed

+266
-0
lines changed

frameworks/Python/panther/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Panther Benchmark Test
2+
3+
This is the Panther portion of a [benchmarking tests suite](../../)
4+
comparing a variety of web development platforms.
5+
6+
The information below is specific to Panther. For further guidance,
7+
review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wiki).
8+
Also note that there is additional information provided in
9+
the [Python README](../).
10+
11+
## Description
12+
13+
[Panther](https://pantherpy.github.io/) Is A Fast & Friendly Web Framework For Building Async APIs With Python 3.10+
14+
<p align="center">
15+
<img src="https://github.com/AliRn76/panther/raw/master/docs/docs/images/logo-vertical.png" alt="logo" style="width: 450px">
16+
</p>
17+
18+
19+
## Implementation
20+
21+
All tests are implemented in a single file ([app.py](app.py)).
22+
23+
* [JSON](app.py): "/json"
24+
* [DB](app.py): "/db"
25+
* [QUERY](app.py): "/queries?=#"*
26+
* [FORTUNES](app.py): "/fortunes"
27+
* [UPDATE](app.py): "/updates?queries=#"*
28+
* [Plaintext](app.py): "/plaintext"
29+
30+
*Replace # with an actual number.
31+
32+
## Resources
33+
34+
* [GitHub](https://github.com/AliRn76/Panther)
35+
* [Documentation](https://pantherpy.github.io)
36+
* [PyPI](https://pypi.org/project/panther)

frameworks/Python/panther/app.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import multiprocessing
2+
import os
3+
from random import randint, sample
4+
5+
import asyncpg
6+
import jinja2
7+
from panther import Panther
8+
from panther.app import API
9+
from panther.request import Request
10+
from panther.response import Response, PlainTextResponse, HTMLResponse
11+
12+
READ_ROW_SQL = 'SELECT "id", "randomnumber" FROM "world" WHERE id = $1'
13+
WRITE_ROW_SQL = 'UPDATE "world" SET "randomnumber"=$1 WHERE id=$2'
14+
ADDITIONAL_ROW = [0, 'Additional fortune added at request time.']
15+
MAX_POOL_SIZE = 1000 // multiprocessing.cpu_count()
16+
MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)
17+
18+
pool = None
19+
20+
21+
async def create_db_pool():
22+
global pool
23+
pool = await asyncpg.create_pool(
24+
user='benchmarkdbuser',
25+
password='benchmarkdbpass',
26+
database='hello_world',
27+
host='tfb-database',
28+
port=5432,
29+
min_size=MIN_POOL_SIZE,
30+
max_size=MAX_POOL_SIZE,
31+
)
32+
33+
34+
async def clean_db_pool():
35+
await pool.close()
36+
37+
38+
def load_fortunes_template():
39+
path = os.path.join('templates', 'fortune.html')
40+
with open(path, 'r') as template_file:
41+
template_text = template_file.read()
42+
return jinja2.Template(template_text)
43+
44+
45+
fortune_template = load_fortunes_template()
46+
47+
48+
def get_num_queries(request):
49+
value = request.query_params.get('queries')
50+
if value is None:
51+
return 1
52+
53+
try:
54+
query_count = int(value)
55+
except ValueError:
56+
return 1
57+
if query_count < 1:
58+
return 1
59+
if query_count > 500:
60+
return 500
61+
return query_count
62+
63+
64+
@API()
65+
async def json_serialization():
66+
return Response(data={'message': 'Hello, world!'})
67+
68+
69+
@API()
70+
async def single_database_query():
71+
row_id = randint(1, 10000)
72+
async with pool.acquire() as connection:
73+
number = await connection.fetchval(READ_ROW_SQL, row_id)
74+
return Response(data={'id': row_id, 'randomNumber': number})
75+
76+
77+
@API()
78+
async def multiple_database_queries(request: Request):
79+
num_queries = get_num_queries(request)
80+
row_ids = sample(range(1, 10000), num_queries)
81+
82+
async with pool.acquire() as connection:
83+
statement = await connection.prepare(READ_ROW_SQL)
84+
worlds = [{'id': i, 'randomNumber': await statement.fetchval(i)} for i in row_ids]
85+
86+
return Response(data=worlds)
87+
88+
89+
@API()
90+
async def fortunes():
91+
async with pool.acquire() as connection:
92+
fortune_records = await connection.fetch('SELECT * FROM Fortune')
93+
fortune_records.append(ADDITIONAL_ROW)
94+
fortune_records.sort(key=lambda row: row[1])
95+
data = fortune_template.render(fortunes=fortune_records)
96+
return HTMLResponse(data=data)
97+
98+
99+
@API()
100+
async def database_updates(request: Request):
101+
num_queries = get_num_queries(request)
102+
ids = sorted(sample(range(1, 10000 + 1), num_queries))
103+
numbers = sorted(sample(range(1, 10000), num_queries))
104+
updates = list(zip(ids, numbers))
105+
106+
worlds = [
107+
{'id': row_id, 'randomNumber': number} for row_id, number in updates
108+
]
109+
110+
async with pool.acquire() as connection:
111+
statement = await connection.prepare(READ_ROW_SQL)
112+
for row_id, _ in updates:
113+
await statement.fetchval(row_id)
114+
await connection.executemany(WRITE_ROW_SQL, updates)
115+
return Response(data=worlds)
116+
117+
118+
@API()
119+
async def plaintext():
120+
return PlainTextResponse(b'Hello, world!')
121+
122+
123+
url_routing = {
124+
'json': json_serialization,
125+
'db': single_database_query,
126+
'queries': multiple_database_queries,
127+
'fortunes': fortunes,
128+
'updates': database_updates,
129+
'plaintext': plaintext,
130+
}
131+
132+
app = Panther(__name__, configs=__name__, urls=url_routing, startup=create_db_pool, shutdown=clean_db_pool)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"framework": "panther",
3+
"tests": [{
4+
"default": {
5+
"json_url": "/json",
6+
"fortune_url": "/fortunes",
7+
"plaintext_url": "/plaintext",
8+
"db_url": "/db",
9+
"query_url": "/queries?queries=",
10+
"update_url": "/updates?queries=",
11+
"port": 8080,
12+
"approach": "Realistic",
13+
"classification": "Micro",
14+
"framework": "panther",
15+
"language": "Python",
16+
"flavor": "Python3",
17+
"platform": "ASGI",
18+
"webserver": "Uvicorn",
19+
"os": "Linux",
20+
"orm": "Raw",
21+
"database_os": "Linux",
22+
"database": "Postgres",
23+
"display_name": "Panther",
24+
"versus": "None",
25+
"notes": ""
26+
}
27+
}]
28+
}

frameworks/Python/panther/config.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[framework]
2+
name = "panther"
3+
4+
[main]
5+
urls.plaintext = "/plaintext"
6+
urls.json = "/json"
7+
urls.db = "/db"
8+
urls.query = "/queries?queries="
9+
urls.update = "/updates?queries="
10+
urls.fortune = "/fortunes"
11+
approach = "Realistic"
12+
classification = "Micro"
13+
database = "Postgres"
14+
database_os = "Linux"
15+
os = "Linux"
16+
orm = "Raw"
17+
platform = "ASGI"
18+
webserver = "Uvicorn"
19+
versus = "None"
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM python:3.11-bullseye
2+
3+
WORKDIR /panther
4+
5+
COPY ./ /panther
6+
7+
RUN pip3 install -U pip
8+
RUN pip3 install -r /panther/requirements.txt
9+
10+
EXPOSE 8080
11+
12+
CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c panther_conf.py
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import multiprocessing
2+
import os
3+
4+
_is_travis = os.environ.get('TRAVIS') == 'true'
5+
6+
workers = multiprocessing.cpu_count()
7+
if _is_travis:
8+
workers = 2
9+
10+
bind = "0.0.0.0:8080"
11+
keepalive = 120
12+
errorlog = '-'
13+
pidfile = '/tmp/panther.pid'
14+
loglevel = 'error'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
panther==3.2.1
2+
3+
cython==3.0.6
4+
jinja2==3.1.2
5+
6+
asyncpg==0.29.0
7+
8+
gunicorn==21.2.0
9+
uvicorn==0.24.0
10+
uvloop==0.19.0
11+
httptools==0.6.1
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Fortunes</title>
5+
</head>
6+
<body>
7+
<table>
8+
<tr><th>id</th><th>message</th></tr>
9+
{% for fortune in fortunes %}
10+
<tr><td>{{ fortune[0] }}</td><td>{{ fortune[1]|e }}</td></tr>
11+
{% endfor %}
12+
</table>
13+
</body>
14+
</html>

0 commit comments

Comments
 (0)