Adding migragtions changing test fixtures

master
androiddrew 6 years ago
parent 7458224fa1
commit ebc198c431

@ -1,3 +1,64 @@
# {{ cookiecutter.project_name }}
{{cookiecutter.description}}
{{cookiecutter.description}}
## First time setup
Create a virtual environment and activate it. Now from the root project directory run `./scripts/bootstrap`. This will install `pip-tools` and sync any dependencies for the first time.
To run the app you will need a [postgres] database. Create a development and a test database. Update the connection strings within the `{{cookiecutter.project_slug}}.settings.toml`. At this time, if you choose to, you can remove the demo `Todo` code and replace it with your own Model. Otherwise create your first [alembic] migration using the `alembic revision --autogenerate -m "your revision message"` command. Finally, apply your first migration with `alembic upgrade head`.
## Running the developement server
A `manage.py` script has been included with a collection of [click] cli functions to assist in development.
__Note__: the developement server command is not a production webserver. You will need to c
```
python manage.py runserver
```
## Using the interactive interpreter
The `manage.py` script can be used to open an interactive interpreter with a configured molten application from your project.
```
python manage.py shell
```
## Dependency management
Dependencies are managed via [pip-tools].
### Adding a dependency
To add a dependency, edit `requirements.in` (or `dev_requirements.in`
for dev dependencies) and add your dependency then run `pip-compile
requirements.in`.
### Syncing dependencies
Run `pip-sync requirements.txt dev_requirements.txt`.
## Migrations
Migrations are managed using [alembic].
### Generating new migrations
alembic revision --autogenerate -m 'message'
### Running the migrations
alembic upgrade head # to upgrade the local db
env ENVIRONMENT=test alembic upgrade head # to upgrade the test db
env ENVIRONMENT=prod alembic upgrade head # to upgrade prod
## Testing
Run the tests by invoking `py.test` in the project root. Make sure you
run any pending migrations beforehand.
[alembic]: http://alembic.zzzcomputing.com/en/latest/
[click]: https://click.palletsprojects.com
[pip-tools]: https://github.com/jazzband/pip-tools
[postgres]: https://www.postgresql.org/

@ -47,14 +47,15 @@ def initdb():
"""
Initialize database
"""
from {{cookiecutter.project_slug}}.db import Base
def _init(engine_data: EngineData):
Base.metadata.create_all(bind=engine_data.engine)
click.echo("Creating database")
app.injector.get_resolver().resolve(_init)()
click.echo("Database created")
click.echo("This feature has been commented out. Please use alembic to manage your database initialization and changes.")
# from {{cookiecutter.project_slug}}.db import Base
#
# def _init(engine_data: EngineData):
# Base.metadata.create_all(bind=engine_data.engine)
#
# click.echo("Creating database")
# app.injector.get_resolver().resolve(_init)()
# click.echo("Database created")
@cli.command()

@ -0,0 +1 @@
Generic single-database configuration.

@ -4,8 +4,8 @@ import os
import sys; sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) # noqa
from alembic import context
from {{cookiecutter.package_name}}.index import create_app
from {{cookiecutter.package_name}}.db import Base
from {{cookiecutter.project_slug}}.index import create_app
from {{cookiecutter.project_slug}}.db import Base
from molten.contrib.sqlalchemy import EngineData
_, app = create_app()

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

@ -3,9 +3,5 @@ molten
sqlalchemy
psycopg2-binary
alembic
{% if cookiecutter.cors_support == 'y' %}
wisgcors
{% endif %}
{% if cookiecutter.static_support == 'y' %}
whitenoise
{% endif %}
{% if cookiecutter.cors_support == 'y' %}wsgicors{% endif %}
{% if cookiecutter.static_support == 'y' %}whitenoise{% endif %}

@ -1,42 +1,51 @@
import pytest
from molten import testing
from molten.contrib.sqlalchemy import EngineData
from molten.contrib.sqlalchemy import Session
from {{cookiecutter.project_slug}}.index import create_app
from {{cookiecutter.project_slug}}.db import Base
def truncate_all_tables(session: Session):
table_names = session.execute("""
select table_name from information_schema.tables
where table_schema = 'public'
and table_type = 'BASE TABLE'
and table_name != 'alembic_version'
""")
for (table_name,) in table_names:
# "truncate" can deadlock so we use delete which is guaranteed not to.
session.execute(f"delete from {table_name}")
session.commit()
# requires function scope so that database is removed on every tests
@pytest.fixture(scope="function")
def app():
@pytest.fixture(scope="session")
def app_global():
_, app = create_app()
yield app
@pytest.fixture(autouse=True)
def create_db(app):
"""Creates a test database with session scope"""
def _retrieve_engine(engine_data: EngineData):
return engine_data.engine
engine = app.injector.get_resolver().resolve(_retrieve_engine)()
Base.metadata.create_all(bind=engine)
@pytest.fixture
def app(app_global):
# This is a little "clever"/piggy. We only want a single instance
# of the app to ever be created, but we also want to ensure that
# the DB is cleared after every test hence "app_global" being a
# session-scoped fixture and this one being test-scoped.
yield app_global
resolver = app_global.injector.get_resolver()
resolver.resolve(truncate_all_tables)()
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
@pytest.fixture
def client(app):
"""Creates a testing client"""
return testing.TestClient(app)
@pytest.fixture(scope="function")
def session():
pass
@pytest.fixture
def load_component(app):
def load(annotation):
def loader(c: annotation):
return c
return app.injector.get_resolver().resolve(loader)()
return load

@ -1,23 +1,22 @@
import os
from typing import Tuple
from molten import App, Route, ResponseRendererMiddleware, Settings
from molten.http import HTTP_404, Request
from molten.openapi import Metadata, OpenAPIHandler, OpenAPIUIHandler
from molten.settings import SettingsComponent
from molten.contrib.sqlalchemy import SQLAlchemyMiddleware, SQLAlchemyEngineComponent, SQLAlchemySessionComponent
{ % if cookiecutter.cors_support == 'y' %}
{%- if cookiecutter.cors_support == 'y' %}
from wsgicors import CORS
{ % endif %}
{ % if cookiecutter.cors_support == 'y' %}
{%- endif %}
{%- if cookiecutter.static_support == 'y' %}
from whitenoise import WhiteNoise
{ % endif %}
{%- endif %}
from .api.welcome import welcome
from .api.todo import TodoManagerComponent, todo_routes
from .common import ExtJSONRenderer
from .logging import setup_logging
from .schema import APIResponse
from .settings import SETTINGS
from . import settings
get_schema = OpenAPIHandler(
metadata=Metadata(
@ -30,7 +29,7 @@ get_schema = OpenAPIHandler(
get_docs = OpenAPIUIHandler()
components = [
SettingsComponent(SETTINGS),
SettingsComponent(settings),
SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(),
TodoManagerComponent(),
@ -82,12 +81,12 @@ def create_app(_components=None, _middleware=None, _routes=None, _renderers=None
renderers=_renderers or renderers
)
{ % if cookiecutter.cors_support == 'y' %}
{%- if cookiecutter.cors_support == 'y' %}
wrapped_app = CORS(wrapped_app, **settings.strict_get("wsgicors"))
{ % endif %}
{%- endif %}
{ % if cookiecutter.static_support == 'y' %}
{%- if cookiecutter.static_support == 'y' %}
wrapped_app = WhiteNoise(wrapped_app, **settings.strict_get("whitenoise"))
{ % endif %}
{%- endif %}
return wrapped_app, app

Loading…
Cancel
Save