Skip to content

Commit

Permalink
Update database step in Quick Tour
Browse files Browse the repository at this point in the history
  • Loading branch information
stevepiercy committed Jan 9, 2021
1 parent 6326c7c commit 3623f35
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 83 deletions.
3 changes: 2 additions & 1 deletion docs/quick_tour/sqla_demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ dist/
nosetests.xml
env*/
tmp/
Data.fs*
Data*.fs*
*.sublime-project
*.sublime-workspace
.*.sw?
.sw?
.DS_Store
coverage
test
*.sqlite
7 changes: 4 additions & 3 deletions docs/quick_tour/sqla_demo/README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ sqla_demo
Getting Started
---------------

- Change directory into your newly created project.
- Change directory into your newly created project if not already there. Your
current directory should be the same as this README.txt file and setup.py.

cd sqla_demo

- Create a Python virtual environment.
- Create a Python virtual environment, if not already created.

python3 -m venv env

- Upgrade packaging tools.
- Upgrade packaging tools, if necessary.

env/bin/pip install --upgrade pip setuptools

Expand Down
2 changes: 1 addition & 1 deletion docs/quick_tour/sqla_demo/pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[pytest]
addopts = --strict
addopts = --strict-markers

testpaths =
sqla_demo
Expand Down
2 changes: 1 addition & 1 deletion docs/quick_tour/sqla_demo/sqla_demo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
with Configurator(settings=settings) as config:
config.include('.models')
config.include('pyramid_jinja2')
config.include('.routes')
config.include('.models')
config.scan()
return config.make_wsgi_app()
22 changes: 15 additions & 7 deletions docs/quick_tour/sqla_demo/sqla_demo/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,21 @@ def includeme(config):
# use pyramid_retry to retry a request when transient exceptions occur
config.include('pyramid_retry')

session_factory = get_session_factory(get_engine(settings))
# hook to share the dbengine fixture in testing
dbengine = settings.get('dbengine')
if not dbengine:
dbengine = get_engine(settings)

session_factory = get_session_factory(dbengine)
config.registry['dbsession_factory'] = session_factory

# make request.dbsession available for use in Pyramid
config.add_request_method(
# r.tm is the transaction manager used by pyramid_tm
lambda r: get_tm_session(session_factory, r.tm),
'dbsession',
reify=True
)
def dbsession(request):
# hook to share the dbsession fixture in testing
dbsession = request.environ.get('app.dbsession')
if dbsession is None:
# request.tm is the transaction manager used by pyramid_tm
dbsession = get_tm_session(session_factory, request.tm)
return dbsession

config.add_request_method(dbsession, reify=True)
7 changes: 3 additions & 4 deletions docs/quick_tour/sqla_demo/sqla_demo/views/default.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from pyramid.view import view_config
from pyramid.response import Response

from sqlalchemy.exc import DBAPIError
from sqlalchemy.exc import SQLAlchemyError

from .. import models

Expand All @@ -10,8 +9,8 @@
def my_view(request):
try:
query = request.dbsession.query(models.MyModel)
one = query.filter(models.MyModel.name == 'one').first()
except DBAPIError:
one = query.filter(models.MyModel.name == 'one').one()
except SQLAlchemyError:
return Response(db_err_msg, content_type='text/plain', status=500)
return {'one': one, 'project': 'sqla_demo'}

Expand Down
79 changes: 79 additions & 0 deletions docs/quick_tour/sqla_demo/testing.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
###
# app configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
###

[app:main]
use = egg:sqla_demo

pyramid.reload_templates = false
pyramid.debug_authorization = false
pyramid.debug_notfound = false
pyramid.debug_routematch = false
pyramid.default_locale_name = en

sqlalchemy.url = sqlite:///%(here)s/testing.sqlite

retry.attempts = 3

[pshell]
setup = sqla_demo.pshell.setup

###
# wsgi server configuration
###

[alembic]
# path to migration scripts
script_location = sqla_demo/alembic
file_template = %%(year)d%%(month).2d%%(day).2d_%%(rev)s
# file_template = %%(rev)s_%%(slug)s

[server:main]
use = egg:waitress#main
listen = localhost:6543

###
# logging configuration
# https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
###

[loggers]
keys = root, sqla_demo, sqlalchemy, alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = INFO
handlers = console

[logger_sqla_demo]
level = DEBUG
handlers =
qualname = sqla_demo

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)

[logger_alembic]
level = WARN
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
132 changes: 132 additions & 0 deletions docs/quick_tour/sqla_demo/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import alembic
import alembic.config
import alembic.command
import os
from pyramid.paster import get_appsettings
from pyramid.scripting import prepare
from pyramid.testing import DummyRequest, testConfig
import pytest
import transaction
import webtest

from sqla_demo import main
from sqla_demo import models
from sqla_demo.models.meta import Base


def pytest_addoption(parser):
parser.addoption('--ini', action='store', metavar='INI_FILE')

@pytest.fixture(scope='session')
def ini_file(request):
# potentially grab this path from a pytest option
return os.path.abspath(request.config.option.ini or 'testing.ini')

@pytest.fixture(scope='session')
def app_settings(ini_file):
return get_appsettings(ini_file)

@pytest.fixture(scope='session')
def dbengine(app_settings, ini_file):
engine = models.get_engine(app_settings)

alembic_cfg = alembic.config.Config(ini_file)
Base.metadata.drop_all(bind=engine)
alembic.command.stamp(alembic_cfg, None, purge=True)

# run migrations to initialize the database
# depending on how we want to initialize the database from scratch
# we could alternatively call:
# Base.metadata.create_all(bind=engine)
# alembic.command.stamp(alembic_cfg, "head")
alembic.command.upgrade(alembic_cfg, "head")

yield engine

Base.metadata.drop_all(bind=engine)
alembic.command.stamp(alembic_cfg, None, purge=True)

@pytest.fixture(scope='session')
def app(app_settings, dbengine):
return main({}, dbengine=dbengine, **app_settings)

@pytest.fixture
def tm():
tm = transaction.TransactionManager(explicit=True)
tm.begin()
tm.doom()

yield tm

tm.abort()

@pytest.fixture
def dbsession(app, tm):
session_factory = app.registry['dbsession_factory']
return models.get_tm_session(session_factory, tm)

@pytest.fixture
def testapp(app, tm, dbsession):
# override request.dbsession and request.tm with our own
# externally-controlled values that are shared across requests but aborted
# at the end
testapp = webtest.TestApp(app, extra_environ={
'HTTP_HOST': 'example.com',
'tm.active': True,
'tm.manager': tm,
'app.dbsession': dbsession,
})

return testapp

@pytest.fixture
def app_request(app, tm, dbsession):
"""
A real request.
This request is almost identical to a real request but it has some
drawbacks in tests as it's harder to mock data and is heavier.
"""
with prepare(registry=app.registry) as env:
request = env['request']
request.host = 'example.com'

# without this, request.dbsession will be joined to the same transaction
# manager but it will be using a different sqlalchemy.orm.Session using
# a separate database transaction
request.dbsession = dbsession
request.tm = tm

yield request

@pytest.fixture
def dummy_request(tm, dbsession):
"""
A lightweight dummy request.
This request is ultra-lightweight and should be used only when the request
itself is not a large focus in the call-stack. It is much easier to mock
and control side-effects using this object, however:
- It does not have request extensions applied.
- Threadlocals are not properly pushed.
"""
request = DummyRequest()
request.host = 'example.com'
request.dbsession = dbsession
request.tm = tm

return request

@pytest.fixture
def dummy_config(dummy_request):
"""
A dummy :class:`pyramid.config.Configurator` object. This allows for
mock configuration, including configuration for ``dummy_request``, as well
as pushing the appropriate threadlocals.
"""
with testConfig(request=dummy_request) as config:
yield config
13 changes: 13 additions & 0 deletions docs/quick_tour/sqla_demo/tests/test_functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from sqla_demo import models

def test_my_view_success(testapp, dbsession):
model = models.MyModel(name='one', value=55)
dbsession.add(model)
dbsession.flush()

res = testapp.get('/', status=200)
assert res.body

def test_notfound(testapp):
res = testapp.get('/badurl', status=404)
assert res.status_code == 404
66 changes: 0 additions & 66 deletions docs/quick_tour/sqla_demo/tests/test_it.py

This file was deleted.

Loading

0 comments on commit 3623f35

Please sign in to comment.