Adding migragtions changing test fixtures

master
androiddrew 6 years ago
parent 7458224fa1
commit ebc198c431

@ -1,3 +1,64 @@
# {{ cookiecutter.project_name }} # {{ 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 Initialize database
""" """
from {{cookiecutter.project_slug}}.db import Base 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) # 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("Creating database")
click.echo("Database created") # app.injector.get_resolver().resolve(_init)()
# click.echo("Database created")
@cli.command() @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 import sys; sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) # noqa
from alembic import context from alembic import context
from {{cookiecutter.package_name}}.index import create_app from {{cookiecutter.project_slug}}.index import create_app
from {{cookiecutter.package_name}}.db import Base from {{cookiecutter.project_slug}}.db import Base
from molten.contrib.sqlalchemy import EngineData from molten.contrib.sqlalchemy import EngineData
_, app = create_app() _, 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 sqlalchemy
psycopg2-binary psycopg2-binary
alembic alembic
{% if cookiecutter.cors_support == 'y' %} {% if cookiecutter.cors_support == 'y' %}wsgicors{% endif %}
wisgcors {% if cookiecutter.static_support == 'y' %}whitenoise{% endif %}
{% endif %}
{% if cookiecutter.static_support == 'y' %}
whitenoise
{% endif %}

@ -1,42 +1,51 @@
import pytest import pytest
from molten import testing 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}}.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") @pytest.fixture(scope="session")
def app(): def app_global():
_, app = create_app() _, app = create_app()
yield app yield app
@pytest.fixture(autouse=True) @pytest.fixture
def create_db(app): def app(app_global):
"""Creates a test database with session scope""" # This is a little "clever"/piggy. We only want a single instance
def _retrieve_engine(engine_data: EngineData): # of the app to ever be created, but we also want to ensure that
return engine_data.engine # the DB is cleared after every test hence "app_global" being a
# session-scoped fixture and this one being test-scoped.
engine = app.injector.get_resolver().resolve(_retrieve_engine)() yield app_global
resolver = app_global.injector.get_resolver()
Base.metadata.create_all(bind=engine) resolver.resolve(truncate_all_tables)()
yield
Base.metadata.drop_all(bind=engine) @pytest.fixture
@pytest.fixture(scope="function")
def client(app): def client(app):
"""Creates a testing client""" """Creates a testing client"""
return testing.TestClient(app) return testing.TestClient(app)
@pytest.fixture
@pytest.fixture(scope="function") def load_component(app):
def session(): def load(annotation):
pass 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 typing import Tuple
from molten import App, Route, ResponseRendererMiddleware, Settings from molten import App, Route, ResponseRendererMiddleware, Settings
from molten.http import HTTP_404, Request from molten.http import HTTP_404, Request
from molten.openapi import Metadata, OpenAPIHandler, OpenAPIUIHandler from molten.openapi import Metadata, OpenAPIHandler, OpenAPIUIHandler
from molten.settings import SettingsComponent from molten.settings import SettingsComponent
from molten.contrib.sqlalchemy import SQLAlchemyMiddleware, SQLAlchemyEngineComponent, SQLAlchemySessionComponent from molten.contrib.sqlalchemy import SQLAlchemyMiddleware, SQLAlchemyEngineComponent, SQLAlchemySessionComponent
{ % if cookiecutter.cors_support == 'y' %} {%- if cookiecutter.cors_support == 'y' %}
from wsgicors import CORS from wsgicors import CORS
{ % endif %} {%- endif %}
{ % if cookiecutter.cors_support == 'y' %} {%- if cookiecutter.static_support == 'y' %}
from whitenoise import WhiteNoise from whitenoise import WhiteNoise
{ % endif %} {%- endif %}
from .api.welcome import welcome from .api.welcome import welcome
from .api.todo import TodoManagerComponent, todo_routes from .api.todo import TodoManagerComponent, todo_routes
from .common import ExtJSONRenderer from .common import ExtJSONRenderer
from .logging import setup_logging from .logging import setup_logging
from .schema import APIResponse from .schema import APIResponse
from .settings import SETTINGS from . import settings
get_schema = OpenAPIHandler( get_schema = OpenAPIHandler(
metadata=Metadata( metadata=Metadata(
@ -30,7 +29,7 @@ get_schema = OpenAPIHandler(
get_docs = OpenAPIUIHandler() get_docs = OpenAPIUIHandler()
components = [ components = [
SettingsComponent(SETTINGS), SettingsComponent(settings),
SQLAlchemyEngineComponent(), SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(), SQLAlchemySessionComponent(),
TodoManagerComponent(), TodoManagerComponent(),
@ -82,12 +81,12 @@ def create_app(_components=None, _middleware=None, _routes=None, _renderers=None
renderers=_renderers or renderers renderers=_renderers or renderers
) )
{ % if cookiecutter.cors_support == 'y' %} {%- if cookiecutter.cors_support == 'y' %}
wrapped_app = CORS(wrapped_app, **settings.strict_get("wsgicors")) 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")) wrapped_app = WhiteNoise(wrapped_app, **settings.strict_get("whitenoise"))
{ % endif %} {%- endif %}
return wrapped_app, app return wrapped_app, app

Loading…
Cancel
Save