소스 검색

update to pyramid 2.0 (#6969)

* update to pyramid 2.0
sqlalchemy 1.4 optimisation

* python 3.10

* randint -> sample for generating unique sequences

* force mark `obj.randomNumber` as modified
Sergey Maranchuk 3 년 전
부모
커밋
1a4e2c6d6a

+ 2 - 2
frameworks/Python/pyramid/README.md

@@ -8,7 +8,7 @@ review the [documentation](https://github.com/TechEmpower/FrameworkBenchmarks/wi
 Also note that there is additional information that's provided in 
 the [Python README](../).
 
-[Pyramid](http://www.pylonsproject.org/) is a flexible Python 2/3 framework.
+[Pyramid](http://www.pylonsproject.org/) is a flexible Python 3 framework.
 This test uses [SQLAlchemy](http://www.sqlalchemy.org/) as its ORM, the default
 [Chameleon](http://www.pylonsproject.org/) for its templating, and
 [Gunicorn](https://github.com/benoitc/gunicorn) for the application server.
@@ -28,5 +28,5 @@ This test uses [SQLAlchemy](http://www.sqlalchemy.org/) as its ORM, the default
 
 ### Community
 
-* `#pyramid` IRC Channel ([irc.freenode.net](https://freenode.net/))
+* `#pyramid` IRC Channel ([irc.libera.chat](https://libera.chat))
 * [Pyramid (pylons-discuss) Google Group](https://groups.google.com/forum/#!forum/pylons-discuss)

+ 0 - 23
frameworks/Python/pyramid/benchmark_config.json

@@ -23,29 +23,6 @@
       "display_name": "Pyramid",
       "notes": "",
       "versus": "wsgi"
-    },
-    "py2": {
-      "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": "Fullstack",
-      "database": "Postgres",
-      "framework": "pyramid",
-      "language": "Python",
-      "flavor": "Python2",
-      "orm": "Full",
-      "platform": "None",
-      "webserver": "Meinheld",
-      "os": "Linux",
-      "database_os": "Linux",
-      "display_name": "Pyramid",
-      "notes": "",
-      "versus": "wsgi"
     }
   }]
 }

+ 0 - 17
frameworks/Python/pyramid/config.toml

@@ -17,20 +17,3 @@ orm = "Full"
 platform = "None"
 webserver = "Meinheld"
 versus = "wsgi"
-
-[py2]
-urls.plaintext = "/plaintext"
-urls.json = "/json"
-urls.db = "/db"
-urls.query = "/queries?queries="
-urls.update = "/updates?queries="
-urls.fortune = "/fortunes"
-approach = "Realistic"
-classification = "Fullstack"
-database = "Postgres"
-database_os = "Linux"
-os = "Linux"
-orm = "Full"
-platform = "None"
-webserver = "Meinheld"
-versus = "wsgi"

+ 0 - 18
frameworks/Python/pyramid/create_database.py

@@ -1,18 +0,0 @@
-import codecs
-import os
-from frameworkbenchmarks.models import DBSession
-
-FWROOT = os.environ.get('FWROOT', '../../..')
-
-if __name__ == "__main__":
-    """
-    Initialize database
-    """
-    with codecs.open('%s/config/create-postgres.sql' % FWROOT,
-                     'r',
-                     encoding='utf-8') as fp:
-        sql = fp.read()
-    DBSession.execute(sql)
-    DBSession.commit()
-
-

+ 5 - 2
frameworks/Python/pyramid/development.ini

@@ -11,15 +11,18 @@ pyramid.debug_authorization = false
 pyramid.debug_notfound = false
 pyramid.debug_routematch = false
 pyramid.default_locale_name = en
+pyramid.includes = pyramid_debugtoolbar
+debugtoolbar.active_panels = performance
+sqlalchemy.url = postgresql:///frameworkbenchmarks
 
 ###
 # wsgi server configuration
 ###
 
 [server:main]
-use = egg:gunicorn#main
+use = egg:waitress#main
 host = 0.0.0.0
-port = 6543
+port = 8080
 
 ###
 # logging configuration

+ 31 - 11
frameworks/Python/pyramid/frameworkbenchmarks/__init__.py

@@ -2,19 +2,39 @@
 App config and initialization.
 """
 
+import orjson
 from pyramid.config import Configurator
+from pyramid.renderers import JSON
+from .models import get_engine, get_session_factory
+
+
+def dbsession(request):
+    sess = request.registry.dbsession_factory()
+
+    def cleanup(request):
+        sess.close()
+
+    request.add_finished_callback(cleanup)
+    return sess
 
 
 def main(global_config, **settings):
-    """ This function returns a Pyramid WSGI application.
-    """
-    config = Configurator(settings=settings)
-    config.include('pyramid_chameleon')
-    config.add_route('test_1', '/json')
-    config.add_route('test_2', '/db')
-    config.add_route('test_3', '/queries')
-    config.add_route('test_4', '/fortunes')
-    config.add_route('test_5', '/updates')
-    config.add_route('test_6', '/plaintext')
-    config.scan()
+    """This function returns a Pyramid WSGI application."""
+    json_renderer = JSON(serializer=orjson.dumps)
+    with Configurator(settings=settings) as config:
+        config.include("pyramid_chameleon")
+        config.add_renderer("json", json_renderer)
+        config.add_route("test_1", "/json")
+        config.add_route("test_2", "/db")
+        config.add_route("test_3", "/queries")
+        config.add_route("test_4", "/fortunes")
+        config.add_route("test_5", "/updates")
+        config.add_route("test_6", "/plaintext")
+        config.set_default_csrf_options(require_csrf=False)
+
+        engine = get_engine(settings)
+        config.registry.dbsession_factory = get_session_factory(engine)
+        config.add_request_method(dbsession, reify=True)
+
+        config.scan()
     return config.make_wsgi_app()

+ 27 - 29
frameworks/Python/pyramid/frameworkbenchmarks/models.py

@@ -2,50 +2,48 @@
 Benchmark models.
 """
 
-import json
-import os
-import psycopg2
-from collections import Iterable
-from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String
-from sqlalchemy.orm import sessionmaker
+
+from sqlalchemy import Column, Integer, MetaData, String, create_engine
+from sqlalchemy.orm import declarative_base, sessionmaker
 from sqlalchemy.pool import QueuePool
-from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta
 
 
-def get_conn():
-    return psycopg2.connect(
-        user='benchmarkdbuser',
-        password='benchmarkdbpass',
-        host='tfb-database',
-        port='5432',
-        database='hello_world')
+def get_engine(settings):
+    return create_engine(
+        settings["sqlalchemy.url"],
+        poolclass=QueuePool,
+        pool_size=100,
+        max_overflow=25,
+        enable_from_linting=False,
+        future=True,
+    )
+
 
+def get_session_factory(engine):
+    Session = sessionmaker(bind=engine, autoflush=False, future=True)
+    return Session
 
-conn_pool = QueuePool(get_conn, pool_size=100, max_overflow=25, echo=False)
 
-pg = create_engine('postgresql://', pool=conn_pool)
-DBSession = sessionmaker(bind=pg)()
 metadata = MetaData()
 
-DatabaseBase = declarative_base()
+Base = declarative_base()
 
 
-class World(DatabaseBase):
-    __tablename__ = 'world'
+class World(Base):
+    __tablename__ = "world"
 
-    id = Column('id', Integer, primary_key=True)
-    randomNumber = Column(
-        'randomnumber', Integer, nullable=False, server_default='0')
+    id = Column("id", Integer, primary_key=True)
+    randomNumber = Column("randomnumber", Integer, nullable=False, server_default="0")
 
     def __json__(self, request=None):
-        return {'id': self.id, 'randomNumber': self.randomNumber}
+        return {"id": self.id, "randomNumber": self.randomNumber}
 
 
-class Fortune(DatabaseBase):
-    __tablename__ = 'fortune'
+class Fortune(Base):
+    __tablename__ = "fortune"
 
-    id = Column('id', Integer, primary_key=True)
-    message = Column('message', String, nullable=False)
+    id = Column("id", Integer, primary_key=True)
+    message = Column("message", String, nullable=False)
 
     def __json__(self):
-        return {'id': self.id, 'message': self.message}
+        return {"id": self.id, "message": self.message}

+ 25 - 31
frameworks/Python/pyramid/frameworkbenchmarks/tests.py

@@ -1,67 +1,61 @@
-# encoding: utf-8
-
 import unittest
 import json
-import sys
+
 
 class FunctionalTests(unittest.TestCase):
     def setUp(self):
         from frameworkbenchmarks import main
-        app = main({})
         from webtest import TestApp
+
+        app = main({}, **{"sqlalchemy.url": "postgresql:///frameworkbenchmarks"})
+
         self.testapp = TestApp(app)
-        self.py3k = sys.version_info >= (3, 0)
 
-    def _get(self, url, content_type='application/json'):
+    def _get(self, url, content_type="application/json"):
         res = self.testapp.get(url, status=200)
-        self.assertTrue('Content-Length' in res.headers)
+        self.assertTrue("Content-Length" in res.headers)
         return res
 
-    def _str_compat(self, obj):
-        if self.py3k:
-            return obj.decode('utf-8')
-        return obj
-
     def _test_obj(self, obj):
-        self.assertTrue('id' in obj)
-        self.assertTrue('randomNumber' in obj)
-        self.assertTrue(1 <= obj['randomNumber'] <= 10000)
+        self.assertTrue("id" in obj)
+        self.assertTrue("randomNumber" in obj)
+        self.assertTrue(1 <= obj["randomNumber"] <= 10000)
 
     def test_json(self):
         """
         /json
         """
-        res = self._get('/json')
-        self.assertEqual(self._str_compat(res.body), """{"message": "Hello, World!"}""")
+        res = self._get("/json")
+        self.assertEqual(res.body, b"""{"message":"Hello, World!"}""")
 
     def test_db(self):
         """
         /db
         """
-        res = self._get('/db')
-        obj = json.loads(self._str_compat(res.body))
+        res = self._get("/db")
+        obj = json.loads(res.body)
         self._test_obj(obj)
 
     def test_queries_0(self):
         """
         /queries?queries=0
         """
-        res = self._get('/queries?queries=0')
-        self.assertEqual(len(json.loads(self._str_compat(res.body))), 1)
+        res = self._get("/queries?queries=0")
+        self.assertEqual(len(json.loads(res.body)), 1)
 
     def test_queries_999(self):
         """
         /queries?queries=999
         """
-        res = self._get('/queries?queries=999')
-        self.assertEqual(len(json.loads(self._str_compat(res.body))), 500)
+        res = self._get("/queries?queries=999")
+        self.assertEqual(len(json.loads(res.body)), 500)
 
     def test_queries_10(self):
         """
         /queries?queries=10 objects
         """
-        res = self._get('/queries?queries=10')
-        objset = json.loads(self._str_compat(res.body))
+        res = self._get("/queries?queries=10")
+        objset = json.loads(res.body)
         for obj in objset:
             self._test_obj(obj)
 
@@ -69,15 +63,15 @@ class FunctionalTests(unittest.TestCase):
         """
         /fortunes
         """
-        res = self._get('/fortunes')
-        self.assertEqual(self._str_compat(res.body).strip(), fortunes.strip())
+        res = self._get("/fortunes")
+        self.assertEqual(res.body.decode('utf-8').strip(), fortunes.strip())
 
     def test_updates(self):
         """
         /updates?queries=10
         """
-        res = self._get('/updates?queries=10')
-        objset = json.loads(self._str_compat(res.body))
+        res = self._get("/updates?queries=10")
+        objset = json.loads(res.body)
         # don't bother with more...
         for obj in objset:
             self._test_obj(obj)
@@ -86,8 +80,8 @@ class FunctionalTests(unittest.TestCase):
         """
         /plaintext
         """
-        res = self._get('/plaintext', content_type='text/plain')
-        self.assertEqual(self._str_compat(res.body), "Hello, World!")
+        res = self._get("/plaintext", content_type="text/plain")
+        self.assertEqual(res.body, b"Hello, World!")
 
 
 fortunes = """

+ 43 - 53
frameworks/Python/pyramid/frameworkbenchmarks/views.py

@@ -3,21 +3,31 @@ Test views, per the spec here:
     http://www.techempower.com/benchmarks/#section=code&hw=i7&test=json
 """
 from operator import itemgetter
-from random import randint
-import sys
+from random import randint, sample
 
-from pyramid.view import view_config
 from pyramid.response import Response
-from frameworkbenchmarks.models import DBSession, World, Fortune
-from sqlalchemy.ext import baked
+from pyramid.view import view_config
+from sqlalchemy import select
+from sqlalchemy.orm.attributes import flag_modified
 
-if sys.version_info[0] == 3:
-    xrange = range
+from frameworkbenchmarks.models import Fortune, World
 
-bakery = baked.bakery()
+
+def parse_query(request):
+    queries = request.GET.get("queries", 1)
+    try:
+        queries = int(queries)
+    except ValueError:
+        queries = 1
+    else:
+        if queries < 1:
+            queries = 1
+        elif queries > 500:
+            queries = 500
+    return queries
 
 
-@view_config(route_name='test_1', renderer='json')
+@view_config(route_name="test_1", renderer="json")
 def test_1(request):
     """
     Test type 1: JSON serialization
@@ -25,87 +35,67 @@ def test_1(request):
     return {"message": "Hello, World!"}
 
 
-@view_config(route_name='test_2', renderer='json')
+@view_config(route_name="test_2", renderer="json")
 def test_2(request):
     """
     Test type 2: Single database query
     """
     num = randint(1, 10000)
-    baked_query = bakery(lambda session: session.query(World))
-    result = baked_query(DBSession).get(num)
+    result = request.dbsession.get(World, num)
     return result.__json__()
 
 
-@view_config(route_name='test_3', renderer='json')
+@view_config(route_name="test_3", renderer="json")
 def test_3(request):
     """
     Test type 3: Multiple database queries
     """
-    queries = request.GET.get('queries', 1)
-    try:
-        queries = int(queries)
-    except ValueError:
-        queries = 1
-    else:
-        if queries < 1:
-            queries = 1
-        elif queries > 500:
-            queries = 500
-    baked_query = bakery(lambda session: session.query(World))
+    queries = parse_query(request)
     result = [
-        baked_query(DBSession).get(num).__json__()
-        for num in (randint(1, 10000) for _ in xrange(queries))
+        request.dbsession.get(World, num).__json__()
+        for num in sample(range(1, 10001), queries)
     ]
     return result
 
 
-@view_config(route_name='test_4', renderer='templates/test_4.pt')
+@view_config(route_name="test_4", renderer="templates/test_4.pt")
 def test_4(request):
     """
     Test type 4: Fortunes
     """
-    baked_query = bakery(lambda session: session.query(Fortune))
-
-    fortunes = [obj.__json__() for obj in baked_query(DBSession).all()]
-    fortunes.append(
-        {"id": 0, "message": "Additional fortune added at request time."}
+    fortunes = (
+        request.dbsession.execute(select(Fortune.id, Fortune.message)).mappings().all()
     )
-    fortunes.sort(key=itemgetter('message'))
-    return {'fortunes': fortunes}
+    fortunes.append({"id": 0, "message": "Additional fortune added at request time."})
+    fortunes.sort(key=itemgetter("message"))
+    return {"fortunes": fortunes}
 
 
-@view_config(route_name='test_5', renderer='json')
+@view_config(route_name="test_5", renderer="json")
 def test_5(request):
     """
     Test type 5: Database updates
     """
-    queries = request.GET.get('queries', 1)
-    try:
-        queries = int(queries)
-    except ValueError:
-        queries = 1
-    else:
-        if queries < 1:
-            queries = 1
-        elif queries > 500:
-            queries = 500
-    baked_query = bakery(lambda session: session.query(World))
+    queries = parse_query(request)
     resultset = []
-    for num in sorted(randint(1, 10000) for _ in xrange(queries)):
-        obj = baked_query(DBSession).get(num)
+    sess = request.dbsession
+    for num in sample(range(1, 10001), queries):
+        obj = sess.get(World, num)
         obj.randomNumber = randint(1, 10000)
+        # force sqlalchemy to UPDATE entry even if the value has not changed
+        # doesn't make sense in a real application, added only for pass `tfb verify`
+        flag_modified(obj, "randomNumber")
         resultset.append(obj.__json__())
-    DBSession.commit()
+    sess.commit()
     return resultset
 
 
-@view_config(route_name='test_6')
+@view_config(route_name="test_6")
 def test_6(request):
     """
     Test type 6: Plaintext
     """
     response = Response(
-        body=b'Hello, World!',
-        content_type='text/plain',
-        )
+        body=b"Hello, World!", content_type="text/plain", charset="utf-8"
+    )
     return response

+ 9 - 14
frameworks/Python/pyramid/gunicorn_conf.py

@@ -1,9 +1,8 @@
 import multiprocessing
 import os
-import sys
 
-_is_pypy = hasattr(sys, 'pypy_version_info')
-_is_travis = os.environ.get('TRAVIS') == 'true'
+
+_is_travis = os.environ.get("TRAVIS") == "true"
 
 workers = multiprocessing.cpu_count() * 3
 if _is_travis:
@@ -11,16 +10,12 @@ if _is_travis:
 
 bind = "0.0.0.0:8080"
 keepalive = 120
-errorlog = '-'
-pidfile = '/tmp/gunicorn.pid'
-
-if _is_pypy:
-    worker_class = "tornado"
-else:
-    worker_class = "meinheld.gmeinheld.MeinheldWorker"
+errorlog = "-"
+pidfile = "/tmp/gunicorn.pid"
 
-    def post_fork(server, worker):
-        # Disalbe access log
-        import meinheld.server
-        meinheld.server.set_access_logger(None)
+worker_class = "meinheld.gmeinheld.MeinheldWorker"
 
+def post_fork(server, worker):
+    # Disalbe access log
+    import meinheld.server
+    meinheld.server.set_access_logger(None)

+ 3 - 2
frameworks/Python/pyramid/production.ini

@@ -11,7 +11,7 @@ pyramid.debug_authorization = false
 pyramid.debug_notfound = false
 pyramid.debug_routematch = false
 pyramid.default_locale_name = en
-
+sqlalchemy.url = postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world
 ###
 # wsgi server configuration
 ###
@@ -19,7 +19,8 @@ pyramid.default_locale_name = en
 [server:main]
 use = egg:gunicorn#main
 host = 0.0.0.0
-port = 6543
+port = 8080
+
 
 ###
 # logging configuration

+ 0 - 11
frameworks/Python/pyramid/pyramid-py2.dockerfile

@@ -1,11 +0,0 @@
-FROM python:2.7.15-stretch
-
-ADD ./ /pyramid
-
-WORKDIR /pyramid
-
-RUN pip install -r /pyramid/requirements.txt
-
-EXPOSE 8080
-
-CMD gunicorn wsgi:app -c gunicorn_conf.py

+ 6 - 4
frameworks/Python/pyramid/pyramid.dockerfile

@@ -1,10 +1,12 @@
-FROM python:3.6.6-stretch
+FROM python:3.10
 
-ADD ./ /pyramid
-
-WORKDIR /pyramid
+ADD ./requirements.txt /pyramid/requirements.txt
 
+# https://github.com/mopemope/meinheld/pull/123
+RUN pip3 install --no-deps "meinheld==1.0.2"
 RUN pip3 install -r /pyramid/requirements.txt
+ADD ./ /pyramid
+WORKDIR /pyramid
 
 EXPOSE 8080
 

+ 5 - 0
frameworks/Python/pyramid/requirements.in

@@ -0,0 +1,5 @@
+gunicorn
+SQLAlchemy[postgresql]
+pyramid
+pyramid_chameleon
+orjson

+ 47 - 7
frameworks/Python/pyramid/requirements.txt

@@ -1,7 +1,47 @@
-psycopg2==2.8.3
-gunicorn==19.9.0
-meinheld==1.0.1
-SQLAlchemy==1.3.5
-pyramid==1.10.4
-pyramid_chameleon==0.3
-greenlet==0.4.15
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+#    pip-compile --output-file=requirements.txt requirements.in
+#
+chameleon==3.9.1
+    # via pyramid-chameleon
+greenlet==1.1.2
+    # via sqlalchemy
+gunicorn==20.1.0
+    # via -r requirements.in
+hupper==1.10.3
+    # via pyramid
+orjson==3.6.5
+    # via -r requirements.in
+pastedeploy==2.1.1
+    # via plaster-pastedeploy
+plaster==1.0
+    # via
+    #   plaster-pastedeploy
+    #   pyramid
+plaster-pastedeploy==0.7
+    # via pyramid
+psycopg2==2.9.2
+    # via sqlalchemy
+pyramid==2.0
+    # via
+    #   -r requirements.in
+    #   pyramid-chameleon
+pyramid-chameleon==0.3
+    # via -r requirements.in
+sqlalchemy[postgresql]==1.4.28
+    # via -r requirements.in
+translationstring==1.4
+    # via pyramid
+venusian==3.0.0
+    # via pyramid
+webob==1.8.7
+    # via pyramid
+zope.deprecation==4.4.0
+    # via pyramid
+zope.interface==5.4.0
+    # via pyramid
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools

+ 0 - 27
frameworks/Python/pyramid/setup.cfg

@@ -1,27 +0,0 @@
-[nosetests]
-match = ^test
-nocapture = 1
-cover-package = frameworkbenchmarks
-with-coverage = 1
-cover-erase = 1
-
-[compile_catalog]
-directory = frameworkbenchmarks/locale
-domain = frameworkbenchmarks
-statistics = true
-
-[extract_messages]
-add_comments = TRANSLATORS:
-output_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
-width = 80
-
-[init_catalog]
-domain = frameworkbenchmarks
-input_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
-output_dir = frameworkbenchmarks/locale
-
-[update_catalog]
-domain = frameworkbenchmarks
-input_file = frameworkbenchmarks/locale/frameworkbenchmarks.pot
-output_dir = frameworkbenchmarks/locale
-previous = true

+ 24 - 26
frameworks/Python/pyramid/setup.py

@@ -1,39 +1,37 @@
 from setuptools import setup, find_packages
 
 requires = [
-    'pyramid',
-    'pyramid_chameleon',
-    'psycopg2',
-    'sqlalchemy',
-    'gunicorn'
+    "pyramid",
+    "pyramid_chameleon",
+    "sqlalchemy[postgresql]",
+    "gunicorn",
+    "orjson",
 ]
 
-tests_require = [
-    'nose-cov',
-    'webtest'
-]
+tests_require = ["webtest"]
 
-setup(name='frameworkbenchmarks',
-      version='0.0',
-      description='FrameworkBenchmarks',
-      classifiers=[
+setup(
+    name="frameworkbenchmarks",
+    version="0.0",
+    description="FrameworkBenchmarks",
+    classifiers=[
         "Programming Language :: Python",
         "Framework :: Pyramid",
         "Topic :: Internet :: WWW/HTTP",
         "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
-        ],
-      author='',
-      author_email='',
-      url='',
-      keywords='web pyramid pylons',
-      packages=find_packages(),
-      include_package_data=True,
-      zip_safe=False,
-      install_requires=requires,
-      tests_require=tests_require,
-      test_suite="frameworkbenchmarks",
-      entry_points="""\
+    ],
+    author="",
+    author_email="",
+    url="",
+    keywords="web pyramid pylons",
+    packages=find_packages(),
+    include_package_data=True,
+    zip_safe=False,
+    install_requires=requires,
+    tests_require=tests_require,
+    test_suite="frameworkbenchmarks",
+    entry_points="""\
       [paste.app_factory]
       main = frameworkbenchmarks:main
       """,
-      )
+)

+ 1 - 1
frameworks/Python/pyramid/wsgi.py

@@ -1,3 +1,3 @@
 from paste.deploy import loadapp
 
-app = loadapp('config:production.ini', relative_to='.')
+app = loadapp("config:production.ini", relative_to=".")