From ebc198c4317d8bfe3e03f6dacab57439f722b5f2 Mon Sep 17 00:00:00 2001 From: androiddrew Date: Sun, 27 Jan 2019 17:46:35 -0500 Subject: [PATCH] Adding migragtions changing test fixtures --- {{cookiecutter.project_slug}}/README.md | 63 ++++++++++++++++++- {{cookiecutter.project_slug}}/manage.py | 17 ++--- .../migrations/README | 1 + .../migrations/env.py | 4 +- .../migrations/script.py.mako | 24 +++++++ {{cookiecutter.project_slug}}/requirements.in | 8 +-- .../tests/conftest.py | 55 +++++++++------- .../{{cookiecutter.project_slug}}/index.py | 21 +++---- 8 files changed, 142 insertions(+), 51 deletions(-) create mode 100644 {{cookiecutter.project_slug}}/migrations/README rename {{cookiecutter.project_slug}}/{{{cookiecutter.project_slug}} => }/migrations/env.py (74%) create mode 100644 {{cookiecutter.project_slug}}/migrations/script.py.mako diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 83c8eb5..445a667 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -1,3 +1,64 @@ # {{ cookiecutter.project_name }} -{{cookiecutter.description}} \ No newline at end of file +{{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/ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/manage.py b/{{cookiecutter.project_slug}}/manage.py index 1ed25e6..a98fe13 100644 --- a/{{cookiecutter.project_slug}}/manage.py +++ b/{{cookiecutter.project_slug}}/manage.py @@ -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() diff --git a/{{cookiecutter.project_slug}}/migrations/README b/{{cookiecutter.project_slug}}/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/{{cookiecutter.project_slug}}/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/migrations/env.py b/{{cookiecutter.project_slug}}/migrations/env.py similarity index 74% rename from {{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/migrations/env.py rename to {{cookiecutter.project_slug}}/migrations/env.py index 2726330..6c65cd6 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/migrations/env.py +++ b/{{cookiecutter.project_slug}}/migrations/env.py @@ -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() diff --git a/{{cookiecutter.project_slug}}/migrations/script.py.mako b/{{cookiecutter.project_slug}}/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/{{cookiecutter.project_slug}}/migrations/script.py.mako @@ -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"} diff --git a/{{cookiecutter.project_slug}}/requirements.in b/{{cookiecutter.project_slug}}/requirements.in index f209c34..ff7462a 100644 --- a/{{cookiecutter.project_slug}}/requirements.in +++ b/{{cookiecutter.project_slug}}/requirements.in @@ -3,9 +3,5 @@ molten sqlalchemy psycopg2-binary alembic -{% if cookiecutter.cors_support == 'y' %} -wisgcors -{% endif %} -{% if cookiecutter.static_support == 'y' %} -whitenoise -{% endif %} \ No newline at end of file +{% if cookiecutter.cors_support == 'y' %}wsgicors{% endif %} +{% if cookiecutter.static_support == 'y' %}whitenoise{% endif %} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/tests/conftest.py b/{{cookiecutter.project_slug}}/tests/conftest.py index 76f256e..5bba6d3 100644 --- a/{{cookiecutter.project_slug}}/tests/conftest.py +++ b/{{cookiecutter.project_slug}}/tests/conftest.py @@ -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 \ No newline at end of file +@pytest.fixture +def load_component(app): + def load(annotation): + def loader(c: annotation): + return c + return app.injector.get_resolver().resolve(loader)() + return load diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/index.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/index.py index 2d82631..f64c33b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/index.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/index.py @@ -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