Skip to content
Merged

Heaven #8538

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions frameworks/Python/heaven/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Heaven Benchmarking Test

This is the Heaven Web Framework implementation of a [benchmarking tests suite](../../)
comparing a variety of web development platforms.

The information below is specific to Heaven Web Framework. 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

[**Heaven**](https://github.com/rayattack/heaven) is a simple, quick to learn, extremely fast (high-performance) framework for building Web Applications with Python 3.6+.

The key features are:

* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to ASGI).

* **Fast to code**: Increase the speed to develop features by about 300% to 500% *.
* **Less bugs**: Reduce about 40% of human (developer) induced errors. *
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
* **Short**: Minimize code duplication. Multiple features from each parameter declaration. Less bugs.
* **Robust**: Get production-ready code. With automatic interactive documentation.
* **Loosely Opinionated**: You choose if you want to Pydantic, UJson or Orjson, SQLAlchemy or AsyncPG - Heaven get's out of your way *very fast*.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* **Loosely Opinionated**: You choose if you want to Pydantic, UJson or Orjson, SQLAlchemy or AsyncPG - Heaven get's out of your way *very fast*.
* **Loosely Opinionated**: You choose if you want to Pydantic, UJson or Orjson, SQLAlchemy or AsyncPG - Heaven gets out of your way *very fast*.


<small>* estimation based on tests on an internal development team, building production applications.</small>

## Test Paths & Sources

All of the test implementations are located within a single file ([app.py](app.py)).

All the tests are based on the ones for ASGI, as Heaven is an ASGI based framework.


## Resources

* [Heaven source code on GitHub](https://github.com/rayattack/heaven)
* [Heaven website - documentation](https://rayattack.github.io/heaven/)
152 changes: 152 additions & 0 deletions frameworks/Python/heaven/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from multiprocessing import cpu_count, pool
from os import getenv
from random import randint, sample

from asyncpg import create_pool
from heaven import Application, Context, Request, Response
from heaven.constants import STARTUP, SHUTDOWN
from orjson import dumps


#############
# constants #
#############
APPLICATION_JSON = 'application/json'
CONTENT_TYPE = 'Content-Type'
POOL = 'pool'
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."]
MAX_POOL_SIZE = 1000//cpu_count()
MIN_POOL_SIZE = max(int(MAX_POOL_SIZE / 2), 1)


#########################################
# HOOKS for the app (shutdown, startup) #
#########################################
async def up_database(app: Application):
pool = await create_pool(
user=getenv("PGUSER", "benchmarkdbuser"),
password=getenv("PGPASS", "benchmarkdbpass"),
database="hello_world",
host="tfb-database",
port=5432,
min_size=MIN_POOL_SIZE,
max_size=MAX_POOL_SIZE,
)
app.keep(POOL, pool)


async def down_database(app: Application):
pool = app.unkeep(POOL)
await pool.close()


################
# Helper utils #
################
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


###############################
# Handlers for the app routes #
###############################
async def database(req: Request, res: Response, ctx: Context):
row_id = randint(1, 10000)
pool = req.app.peek(POOL)
async with pool.acquire() as connection:
number = await connection.fetchval(READ_ROW_SQL, row_id)

res.headers = CONTENT_TYPE, APPLICATION_JSON
res.body = dumps({"id": row_id, "randomNumber": number})


async def json(req: Request, res: Response, ctx: Context):
res.headers = CONTENT_TYPE, APPLICATION_JSON
res.body = dumps({'message': 'Hello, World!'})


async def queries(req: Request, res: Response, ctx: Context):
pool = req.app.peek(POOL)
queries = req.params.get('queries')
num_queries = get_num_queries(queries)
row_ids = sample(range(1, 10001), num_queries)
worlds = []

async with 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})

res.headers = CONTENT_TYPE, APPLICATION_JSON
res.body = dumps(worlds)


async def fortunes(req: Request, res: Response, ctx: Context):
pool = req.app.peek(POOL)
async with pool.acquire() as connection:
fortunes = await connection.fetch("SELECT * FROM Fortune")

fortunes.append(ADDITIONAL_ROW)
fortunes.sort(key=lambda row: row[1])
await res.render("fortune.html", fortunes=fortunes, request=req)


async def updates(req: Request, res: Response, ctx: Context):
pool = req.app.peek(POOL)
queries = req.params.get('queries')
num_queries = get_num_queries(queries)

# 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 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)
res.headers = CONTENT_TYPE, APPLICATION_JSON
res.body = dumps(worlds)


async def plaintext(req: Request, res: Response, ctx: Context):
res.headers = 'Content-Type', 'text/plain'
res.body = b"Hello, World!"


################
# App creation #
################
app = Application()
app.TEMPLATES('templates')


###################
# Register hooks #
###################
app.ON(STARTUP, up_database)
app.ON(SHUTDOWN, down_database)


###################
# Register routes #
###################
app.GET('/db', database)
app.GET('/queries', queries)
app.GET('/fortunes', fortunes)
app.GET('/updates', updates)
app.GET('/plaintext', plaintext)
app.GET('/json', json)
30 changes: 30 additions & 0 deletions frameworks/Python/heaven/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"framework": "heaven",
"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",
"framework": "Heaven",
"language": "Python",
"flavor": "Python3",
"platform": "None",
"webserver": "None",
"os": "Linux",
"orm": "Raw",
"database_os": "Linux",
"database": "Postgres",
"display_name": "Heaven",
"versus": "None",
"notes": ""
}
}
]
}
19 changes: 19 additions & 0 deletions frameworks/Python/heaven/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[framework]
name = "heaven"

[main]
urls.plaintext = "/plaintext"
urls.json = "/json"
urls.db = "/db"
urls.query = "/queries?queries="
urls.update = "/updates?queries="
urls.fortune = "/fortunes"
approach = "Realistic"
classification = "Platform"
database = "Postgres"
database_os = "Linux"
os = "Linux"
orm = "Raw"
platform = "None"
webserver = "None"
versus = "None"
18 changes: 18 additions & 0 deletions frameworks/Python/heaven/heaven.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM python:3.11

WORKDIR /heaven

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

RUN pip3 install cython==0.29.36

COPY requirements.txt ./

RUN pip3 install -r requirements.txt

COPY . ./

EXPOSE 8080

CMD gunicorn app:app -k uvicorn.workers.UvicornWorker -c heaven_conf.py
12 changes: 12 additions & 0 deletions frameworks/Python/heaven/heaven_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import multiprocessing
import os

_is_travis = os.environ.get('TRAVIS') == 'true'

workers = multiprocessing.cpu_count()

bind = "0.0.0.0:8080"
keepalive = 120
errorlog = '-'
pidfile = '/tmp/heaven.pid'
loglevel = 'error'
4 changes: 4 additions & 0 deletions frameworks/Python/heaven/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
asyncpg==0.29.0
heaven==0.2.4
orjson==3.9.10
gunicorn==20.1.0
10 changes: 10 additions & 0 deletions frameworks/Python/heaven/templates/fortune.html
Original file line number Diff line number Diff line change
@@ -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>