Browse Source

Add Morepath framework to Python (#2903)

Henri Hulski 8 years ago
parent
commit
bed1da66fe

+ 1 - 0
.travis.yml

@@ -160,6 +160,7 @@ env:
     - "TESTDIR=Python/flask"
     - "TESTDIR=Python/historical"
     - "TESTDIR=Python/klein"
+    - "TESTDIR=Python/morepath"
     - "TESTDIR=Python/pyramid"
     - "TESTDIR=Python/tornado"
     - "TESTDIR=Python/turbogears"

+ 6 - 0
frameworks/Python/morepath/.gitignore

@@ -0,0 +1,6 @@
+.venv
+*.egg-info
+__pycache__
+.coverage
+htmlcov
+.cache

+ 50 - 0
frameworks/Python/morepath/README.md

@@ -0,0 +1,50 @@
+# [Morepath](http://morepath.readthedocs.io/) Benchmark Test
+
+The information below is specific to Morepath. For further guidance,
+review the [documentation](http://frameworkbenchmarks.readthedocs.org/en/latest/).
+Also note that there is additional information that's provided in
+the [Python README](../).
+
+This is the Python Morepath portion of a [benchmarking tests suite](../../)
+comparing a variety of frameworks.
+
+All test implementations are located within ([./app](app)).
+
+## Description
+
+Morepath with [PonyOrm](https://docs.ponyorm.com/) using PostgreSQL for
+database access.
+
+### Database
+
+PostgreSQL (with PonyORM).
+
+### Server
+
+gunicorn + meinheld on CPython
+
+## Test URLs
+
+### Test 1: JSON Encoding
+
+    http://localhost:8080/json
+
+### Test 2: Single Row Query
+
+    http://localhost:8080/db
+
+### Test 3: Multi Row Query
+
+    http://localhost:8080/queries?queries=20
+
+### Test 4: Fortunes (Template rendering)
+
+    http://localhost:8080/fortunes
+
+### Test 5: Update Query
+
+    http://localhost:8080/updates?queries=20
+
+### Test 6: Plaintext
+
+    http://localhost:8080/plaintext

+ 3 - 0
frameworks/Python/morepath/app/__init__.py

@@ -0,0 +1,3 @@
+# flake8: noqa
+
+from app.app import App

+ 11 - 0
frameworks/Python/morepath/app/app.py

@@ -0,0 +1,11 @@
+from more.pony import PonyApp
+from more.jinja2 import Jinja2App
+
+
+class App(PonyApp, Jinja2App):
+    pass
+
+
[email protected]_directory()
+def get_template_directory():
+    return 'templates'

+ 6 - 0
frameworks/Python/morepath/app/collection.py

@@ -0,0 +1,6 @@
+from .model import Fortune
+
+
+class FortuneCollection(object):
+    def query(self):
+        return Fortune.select()

+ 29 - 0
frameworks/Python/morepath/app/model.py

@@ -0,0 +1,29 @@
+from pony.orm import Database, Optional
+
+db = Database()
+
+
+class Json():
+    pass
+
+
+class World(db.Entity):
+    randomnumber = Optional(int)
+
+
+class WorldQueries():
+    def __init__(self, queries):
+        self.queries = queries
+
+
+class Fortune(db.Entity):
+    message = Optional(str)
+
+
+class WorldUpdates():
+    def __init__(self, queries):
+        self.queries = queries
+
+
+class Plaintext():
+    pass

+ 35 - 0
frameworks/Python/morepath/app/path.py

@@ -0,0 +1,35 @@
+from random import randint
+
+from .app import App
+from .model import Json, World, WorldQueries, WorldUpdates, Plaintext
+from .collection import FortuneCollection
+
+
[email protected](model=Json, path='json')
+def get_json():
+    return Json()
+
+
[email protected](model=World, path='db')
+def get_random_world():
+    return World[randint(1, 10000)]
+
+
[email protected](model=WorldQueries, path='queries')
+def get_queries(queries):
+    return WorldQueries(queries)
+
+
[email protected](model=FortuneCollection, path='fortunes')
+def get_fortunes():
+    return FortuneCollection()
+
+
[email protected](model=WorldUpdates, path='updates')
+def get_updates(queries):
+    return WorldUpdates(queries)
+
+
[email protected](model=Plaintext, path='plaintext')
+def get_plaintext():
+    return Plaintext()

+ 31 - 0
frameworks/Python/morepath/app/run.py

@@ -0,0 +1,31 @@
+import os
+
+import morepath
+
+from app import App
+from .model import db
+
+
+def setup_db():
+    DBHOST = os.environ.get('DBHOST', 'localhost')
+
+    db.bind(
+        'postgres',
+        user='benchmarkdbuser',
+        password='benchmarkdbpass',
+        host=DBHOST,
+        database='hello_world'
+    )
+    db.generate_mapping(create_tables=True)
+
+
+def wsgi_factory():   # pragma: no cover
+    morepath.autoscan()
+
+    App.commit()
+    setup_db()
+
+    return App()
+
+
+application = wsgi_factory()   # pragma: no cover

+ 21 - 0
frameworks/Python/morepath/app/templates/fortune.jinja2

@@ -0,0 +1,21 @@
+<!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|escape }}</td>
+</tr>
+{% endfor %}
+</table>
+</body>
+</html>

+ 259 - 0
frameworks/Python/morepath/app/tests/test_app.py

@@ -0,0 +1,259 @@
+from webtest import TestApp as Client
+import morepath
+
+import app
+from app import App
+
+
+def setup_module(module):
+    morepath.scan(app)
+    morepath.commit(App)
+
+
+def test_json():
+    """/json"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/json', status=200)
+    assert response.headerlist == [
+        ('Content-Type', 'application/json'),
+        ('Content-Length', '27')
+    ]
+    assert response.json == {"message": "Hello, World!"}
+
+
+def test_db():
+    """/db"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/db', status=200)
+    assert response.content_type == 'application/json'
+    assert 'id' in response.json
+    assert 'randomNumber' in response.json
+    assert 1 <= response.json['id'] <= 10000
+    assert 1 <= response.json['randomNumber'] <= 10000
+
+
+def test_queries():
+    """/queries?queries="""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/queries?queries=', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_queries_foo():
+    """/queries?queries=foo"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/queries?queries=foo', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_queries_0():
+    """/queries?queries=0"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/queries?queries=0', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_queries_999():
+    """/queries?queries=999"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/queries?queries=999', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 500
+
+
+def test_queries_10():
+    """/queries?queries=10"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/queries?queries=10', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 10
+
+    obj_list = response.json
+    for obj in obj_list:
+        assert 'id' in obj
+        assert 'randomNumber' in obj
+        assert 1 <= obj['id'] <= 10000
+        assert 1 <= obj['randomNumber'] <= 10000
+
+
+def test_fortunes():
+    """/fortunes"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/fortunes', status=200)
+    assert response.headerlist == [
+        ('Content-Type', 'text/html; charset=UTF-8'),
+        ('Content-Length', '1304')
+    ]
+    assert response.text == fortunes
+
+
+def test_updates():
+    """/updates?queries="""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/updates?queries=', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_updates_foo():
+    """/updates?queries=foo"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/updates?queries=foo', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_updates_0():
+    """/updates?queries=0"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/updates?queries=0', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 1
+
+
+def test_updates_999():
+    """/updates?queries=999"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/updates?queries=999', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 500
+
+
+def test_updates_10():
+    """/updates?queries=10"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/updates?queries=10', status=200)
+    assert response.content_type == 'application/json'
+    assert len(response.json) == 10
+
+    obj_list = response.json
+    for obj in obj_list:
+        assert 'id' in obj
+        assert 'randomNumber' in obj
+        assert 1 <= obj['id'] <= 10000
+        assert 1 <= obj['randomNumber'] <= 10000
+
+
+def test_plaintext():
+    """/plaintext"""
+    app = App()
+    c = Client(app)
+
+    response = c.get('/plaintext', status=200)
+    assert response.headerlist == [
+        ('Content-Type', 'text/plain; charset=UTF-8'),
+        ('Content-Length', '13')
+    ]
+    assert response.text == 'Hello, World!'
+
+
+fortunes = """<!DOCTYPE html>
+
+<html>
+<head>
+<title>Fortunes</title>
+</head>
+<body>
+<table>
+<tr>
+<th>id</th>
+<th>message</th>
+</tr>
+
+<tr>
+<td>11</td>
+<td>&lt;script&gt;alert(&#34;This should not be displayed in a browser alert box.&#34;);&lt;/script&gt;</td>
+</tr>
+
+<tr>
+<td>4</td>
+<td>A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1</td>
+</tr>
+
+<tr>
+<td>5</td>
+<td>A computer program does what you tell it to do, not what you want it to do.</td>
+</tr>
+
+<tr>
+<td>2</td>
+<td>A computer scientist is someone who fixes things that aren&#39;t broken.</td>
+</tr>
+
+<tr>
+<td>8</td>
+<td>A list is only as strong as its weakest link. — Donald Knuth</td>
+</tr>
+
+<tr>
+<td>0</td>
+<td>Additional fortune added at request time.</td>
+</tr>
+
+<tr>
+<td>3</td>
+<td>After enough decimal places, nobody gives a damn.</td>
+</tr>
+
+<tr>
+<td>7</td>
+<td>Any program that runs right is obsolete.</td>
+</tr>
+
+<tr>
+<td>10</td>
+<td>Computers make very fast, very accurate mistakes.</td>
+</tr>
+
+<tr>
+<td>6</td>
+<td>Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen</td>
+</tr>
+
+<tr>
+<td>9</td>
+<td>Feature: A bug with seniority.</td>
+</tr>
+
+<tr>
+<td>1</td>
+<td>fortune: No such file or directory</td>
+</tr>
+
+<tr>
+<td>12</td>
+<td>フレームワークのベンチマーク</td>
+</tr>
+
+</table>
+</body>
+</html>"""

+ 79 - 0
frameworks/Python/morepath/app/view.py

@@ -0,0 +1,79 @@
+from random import randint
+
+from .app import App
+from .model import Json, World, WorldQueries, WorldUpdates, Plaintext
+from .collection import FortuneCollection
+
+
[email protected](model=Json)
+def test_1(self, request):
+    """Test 1: JSON serialization"""
+    return {'message': 'Hello, World!'}
+
+
[email protected](model=World)
+def test_2(self, request):
+    """Test 2: Single database query"""
+    return {'id': self.id, 'randomNumber': self.randomnumber}
+
+
[email protected](model=WorldQueries)
+def test_3(self, request):
+    """Test 3: Multiple database queries"""
+    try:
+        queries = int(self.queries)
+    except ValueError:
+        queries = 1
+    else:
+        if queries < 1:
+            queries = 1
+        elif queries > 500:
+            queries = 500
+
+    result = []
+
+    for id_ in [randint(1, 10000) for _ in range(queries)]:
+        result.append({'id': id_, 'randomNumber': World[id_].randomnumber})
+
+    return result
+
+
[email protected](model=FortuneCollection, template='fortune.jinja2')
+def test_4(self, request):
+    """Test 4: Fortunes"""
+    fortunes = [f.to_dict() for f in self.query()]
+    fortunes.append({
+        'id': 0,
+        'message': 'Additional fortune added at request time.'
+    })
+
+    return {'fortunes': sorted(fortunes, key=lambda x: x['message'])}
+
+
[email protected](model=WorldUpdates)
+def test_5(self, request):
+    """Test 5: Database updates"""
+    try:
+        queries = int(self.queries)
+    except ValueError:
+        queries = 1
+    else:
+        if queries < 1:
+            queries = 1
+        elif queries > 500:
+            queries = 500
+
+    result = []
+
+    for id_ in sorted(randint(1, 10000) for _ in range(queries)):
+        randomNumber = randint(1, 10000)
+        World[id_].randomnumber = randomNumber
+        result.append({'id': id_, 'randomNumber': randomNumber})
+
+    return result
+
+
[email protected](model=Plaintext)
+def test_6(self, request):
+    """Test 6: Plaintext"""
+    return 'Hello, World!'

+ 28 - 0
frameworks/Python/morepath/benchmark_config.json

@@ -0,0 +1,28 @@
+{
+  "framework": "morepath",
+  "tests": [{
+    "default": {
+      "setup_file": "setup",
+      "json_url": "/json",
+      "db_url": "/db",
+      "query_url": "/queries?queries=",
+      "fortune_url": "/fortunes",
+      "update_url": "/updates?queries=",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "morepath",
+      "language": "Python",
+      "flavor": "Python3",
+      "orm": "Full",
+      "platform": "Meinheld",
+      "webserver": "gunicorn",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "Morepath",
+      "notes": "uses Morepath with PonyORM for database access"
+    }
+  }]
+}

+ 14 - 0
frameworks/Python/morepath/gunicorn_conf.py

@@ -0,0 +1,14 @@
+import multiprocessing
+import os
+
+if os.environ.get('TRAVIS') == 'true':
+    workers = 2
+else:
+    workers = multiprocessing.cpu_count() * 3
+
+bind = '0.0.0.0:8080'
+keepalive = 120
+errorlog = '-'
+pidfile = 'gunicorn.pid'
+
+worker_class = "meinheld.gmeinheld.MeinheldWorker"

+ 17 - 0
frameworks/Python/morepath/requirements.txt

@@ -0,0 +1,17 @@
+dectate==0.13
+greenlet==0.4.12
+gunicorn==19.7.1
+importscan==0.1
+Jinja2==2.9.6
+MarkupSafe==1.0
+meinheld==0.6.1
+more.jinja2==0.2
+more.pony==0.1
+morepath==0.18.1
+pony==0.7.1
+psycopg2==2.7.1
+reg==0.11
+repoze.lru==0.6
+WebOb==1.7.2
+
+-e .

+ 41 - 0
frameworks/Python/morepath/setup.py

@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+from setuptools import setup, find_packages
+
+setup(
+    name='frameworkbenchmarks',
+    version='0.0',
+    description='FrameworkBenchmarks',
+    author='',
+    author_email='',
+    url='',
+    packages=find_packages(),
+    include_package_data=True,
+    zip_safe=False,
+    platforms='any',
+    install_requires=[
+        'more.pony',
+        'psycopg2',
+        'more.jinja2',
+        'gunicorn',
+        'meinheld',
+    ],
+    extras_require=dict(
+        test=[
+            'pytest >= 2.9.1',
+            'WebTest >= 2.0.14',
+            'pytest-cov',
+        ]
+    ),
+    entry_points=dict(
+        morepath=[
+            'scan = app',
+        ],
+    ),
+    classifiers=[
+        'Programming Language :: Python',
+        'Framework :: Morepath',
+        'Topic :: Internet :: WWW/HTTP',
+        'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
+    ]
+)

+ 7 - 0
frameworks/Python/morepath/setup.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+
+fw_depends postgresql python3
+
+pip3 install --install-option="--prefix=${PY3_ROOT}" -r $TROOT/requirements.txt
+
+gunicorn app.run -c gunicorn_conf.py &

+ 8 - 0
frameworks/Python/morepath/source_code

@@ -0,0 +1,8 @@
+
+./morepath/app/__init__.py
+./morepath/app/app.py
+./morepath/app/model.py
+./morepath/app/path.py
+./morepath/app/run.py
+./morepath/app/view.py
+./morepath/app/templates/fortune.jinja2