Browse Source

adds BlackSheep Python framework (#4458)

Roberto Prevato 6 years ago
parent
commit
05fa1758b0

+ 38 - 0
frameworks/Python/blacksheep/README.md

@@ -0,0 +1,38 @@
+# BlackSheep Benchmark Test
+
+This is the BlackSheep portion of a [benchmarking tests suite](../../)
+comparing a variety of web development platforms.
+
+The information below is specific to BlackSheep. For further guidance,
+review the [documentation](http://frameworkbenchmarks.readthedocs.org/en/latest/).
+Also note that there is additional information provided in
+the [Python README](../).
+
+## Description
+
+[BlackSheep](https://github.com/RobertoPrevato/BlackSheep) is a fast HTTP Server/Client microframework for Python [asyncio](https://docs.python.org/3/library/asyncio.html), using [Cython](https://cython.org), 
+[`uvloop`](https://magic.io/blog/uvloop-blazing-fast-python-networking/), and 
+[`httptools`](https://github.com/MagicStack/httptools). 
+
+<p align="left">
+  <a href="#blacksheep"><img width="320" height="271" src="https://raw.githubusercontent.com/RobertoPrevato/BlackSheep/master/black-sheep.svg?sanitize=true" alt="Black Sheep"></a>
+</p>
+
+
+## Implementation
+
+BlackSheep is implemented using:
+
+* [asyncio](https://docs.python.org/3/library/asyncio.html).
+* [Cython](https://cython.org)
+* [`uvloop`](https://magic.io/blog/uvloop-blazing-fast-python-networking/).
+* [`httptools`](https://github.com/MagicStack/httptools).
+* Python built-in multiprocessing module.
+
+## Test Paths & Sources
+
+All of the test implementations are located within a single file ([app.py](app.py)).
+
+## Resources
+
+* [Repo](https://github.com/RobertoPrevato/BlackSheep)

+ 157 - 0
frameworks/Python/blacksheep/app.py

@@ -0,0 +1,157 @@
+import os
+import ujson
+import asyncpg
+from random import randint
+from multiprocessing import cpu_count
+from blacksheep.server import Application, ServerOptions
+from blacksheep import Response, Headers, Header, Content
+from jinja2 import Environment, PackageLoader, select_autoescape
+json_dumps = ujson.dumps
+
+
+_is_travis = os.environ.get('TRAVIS') == 'true'
+
+workers = cpu_count()
+if _is_travis:
+    workers = 2
+
+
+db_pool = None
+
+
+async def configure_db(app):
+    global db_pool
+    db_pool = await asyncpg.create_pool(
+        user=os.getenv('PGUSER', 'benchmarkdbuser'),
+        password=os.getenv('PGPASS', 'benchmarkdbpass'),
+        database='hello_world',
+        host='tfb-database',
+        port=5432
+    )
+
+
+jinja_env = Environment(
+    loader=PackageLoader('app', 'templates'),
+    autoescape=select_autoescape(['html', 'xml'])
+)
+fortune_template = jinja_env.get_template('fortune.html')
+
+
+app = Application(options=ServerOptions(host='', port=8080, processes_count=workers))
+
+
+app.on_start += configure_db
+
+
+def get_num_queries(request):
+    try:
+        value = request.query.get('queries')
+        if value is None:
+            return 1
+
+        query_count = int(value[0])
+    except (KeyError, IndexError, ValueError):
+        return 1
+
+    if query_count < 1:
+        return 1
+    if query_count > 500:
+        return 500
+    return query_count
+
+
[email protected]('/json')
+async def json_test(request):
+    """Test type 1: JSON Serialization"""
+
+    return Response(200, content=Content(b'application/json; charset=utf-8',
+                                         json_dumps({'message': 'Hello, world!'}).encode('utf-8')))
+
+
[email protected]('/db')
+async def single_db_query_test(request):
+    """Test type 2: Single Database Query"""
+
+    row_id = randint(1, 10000)
+    connection = await db_pool.acquire()
+    try:
+        number = await connection.fetchval('SELECT "randomnumber" FROM "world" WHERE id = $1', row_id)
+        world = {'id': row_id, 'randomNumber': number}
+    finally:
+        await db_pool.release(connection)
+
+    return Response(200, content=Content(b'application/json; charset=utf-8',
+                                         json_dumps(world).encode('utf-8')))
+
+
[email protected]('/queries')
+async def multiple_db_queries_test(request):
+    """Test type 3: Multiple Database Queries"""
+
+    num_queries = get_num_queries(request)
+
+    row_ids = [randint(1, 10000) for _ in range(num_queries)]
+    worlds = []
+
+    connection = await db_pool.acquire()
+    try:
+        statement = await connection.prepare('SELECT "randomnumber" FROM "world" WHERE id = $1')
+        for row_id in row_ids:
+            number = await statement.fetchval(row_id)
+            worlds.append({'id': row_id, 'randomNumber': number})
+    finally:
+        await db_pool.release(connection)
+    
+    return Response(200, content=Content(b'application/json; charset=utf-8',
+                                         json_dumps(worlds).encode('utf-8')))
+
+
[email protected]('/fortunes')
+async def fortunes_test(request):
+    """Test type 4: Fortunes"""
+
+    connection = await db_pool.acquire()
+
+    try:
+        fortunes = await connection.fetch('SELECT * FROM Fortune')
+    finally:
+        await db_pool.release(connection)
+
+    fortunes.append([0, 'Additional fortune added at request time.'])
+    fortunes.sort(key=lambda x: x[1])
+
+    return Response(200, Headers([
+        Header(b'Cache-Control', b'no-cache')
+    ]), content=Content(b'text/html; charset=utf-8', fortune_template.render(fortunes=fortunes).encode('utf8')))
+
+
[email protected]('/updates')
+async def db_updates_test(request):
+    """Test type 5: Database Updates"""
+
+    num_queries = get_num_queries(request)
+
+    updates = [(randint(1, 10000), randint(1, 10000)) for _ in range(num_queries)]
+    worlds = [{'id': row_id, 'randomNumber': number} for row_id, number in updates]
+
+    connection = await db_pool.acquire()
+    try:
+        statement = await connection.prepare('SELECT "randomnumber" FROM "world" WHERE id = $1')
+        for row_id, _ in updates:
+            await statement.fetchval(row_id)
+        await connection.executemany('UPDATE "world" SET "randomnumber"=$1 WHERE id=$2', updates)
+    finally:
+        await db_pool.release(connection)
+
+    return Response(200, content=Content(b'application/json',
+                                         json_dumps(worlds).encode('utf-8')))
+
+
[email protected]('/plaintext')
+async def plaintext_test(request):
+    """Test type 6: Plaintext"""
+
+    return Response(200, content=Content(b'text/plain', b'Hello, World!'))
+
+
+app.start()

+ 27 - 0
frameworks/Python/blacksheep/benchmark_config.json

@@ -0,0 +1,27 @@
+{
+  "framework": "blacksheep",
+  "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": "Platform",
+      "framework": "blacksheep",
+      "language": "Python",
+      "flavor": "Python3",
+      "platform": "None",
+      "webserver": "None",
+      "os": "Linux",
+      "orm": "Raw",
+      "database_os": "Linux",
+      "database": "Postgres",
+      "display_name": "blacksheep",
+      "notes": ""
+    }
+  }]
+}

+ 9 - 0
frameworks/Python/blacksheep/blacksheep.dockerfile

@@ -0,0 +1,9 @@
+FROM python:3.7.2-stretch
+
+ADD ./ /blacksheep
+
+WORKDIR /blacksheep
+
+RUN pip3 install -r /blacksheep/requirements.txt
+
+CMD python app.py -O

+ 9 - 0
frameworks/Python/blacksheep/requirements.txt

@@ -0,0 +1,9 @@
+asyncpg==0.18.3
+blacksheep==0.0.7
+cchardet==2.1.4
+certifi==2018.11.29
+httptools==0.0.11
+Jinja2==2.10
+MarkupSafe==1.1.0
+ujson==1.35
+uvloop==0.12.1

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