Merge branch 'develop'
						commit
						0936ce52ae
					
				@ -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/
 | 
			
		||||
@ -0,0 +1,2 @@
 | 
			
		||||
[alembic]
 | 
			
		||||
script_location = migrations
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
#
 | 
			
		||||
# This file is autogenerated by pip-compile
 | 
			
		||||
# To update, run:
 | 
			
		||||
#
 | 
			
		||||
#    pip-compile --output-file dev_requirements.txt dev_requirements.in
 | 
			
		||||
#
 | 
			
		||||
--trusted-host pypi.python.org
 | 
			
		||||
 | 
			
		||||
appdirs==1.4.3            # via black
 | 
			
		||||
atomicwrites==1.2.1       # via pytest
 | 
			
		||||
attrs==18.2.0             # via black, pytest
 | 
			
		||||
black==18.9b0
 | 
			
		||||
bumpversion==0.5.3
 | 
			
		||||
click==7.0                # via black, pip-tools
 | 
			
		||||
coverage==4.5.2           # via pytest-cov
 | 
			
		||||
flake8==3.6.0
 | 
			
		||||
mccabe==0.6.1             # via flake8
 | 
			
		||||
more-itertools==4.3.0     # via pytest
 | 
			
		||||
pip-tools==3.1.0
 | 
			
		||||
pluggy==0.8.0             # via pytest
 | 
			
		||||
py==1.7.0                 # via pytest
 | 
			
		||||
pycodestyle==2.4.0        # via flake8
 | 
			
		||||
pyflakes==2.0.0           # via flake8
 | 
			
		||||
pytest-cov==2.6.0
 | 
			
		||||
pytest==4.0.1
 | 
			
		||||
six==1.11.0               # via more-itertools, pip-tools, pytest
 | 
			
		||||
toml==0.10.0              # via black
 | 
			
		||||
werkzeug==0.14.1
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
Generic single-database configuration.
 | 
			
		||||
@ -0,0 +1,21 @@
 | 
			
		||||
"""isort:skip_file
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import sys; sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), ".."))  # noqa
 | 
			
		||||
 | 
			
		||||
from alembic import context
 | 
			
		||||
from {{cookiecutter.project_slug}}.index import create_app
 | 
			
		||||
from {{cookiecutter.project_slug}}.db import Base
 | 
			
		||||
from molten.contrib.sqlalchemy import EngineData
 | 
			
		||||
 | 
			
		||||
_, app = create_app()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_migrations_online(engine_data: EngineData):
 | 
			
		||||
    with engine_data.engine.connect() as connection:
 | 
			
		||||
        context.configure(connection=connection, target_metadata=Base.metadata)
 | 
			
		||||
        with context.begin_transaction():
 | 
			
		||||
            context.run_migrations()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
app.injector.get_resolver().resolve(run_migrations_online)()
 | 
			
		||||
@ -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"}
 | 
			
		||||
@ -1,4 +1,7 @@
 | 
			
		||||
click
 | 
			
		||||
molten
 | 
			
		||||
sqlalchemy
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
psycopg2-binary
 | 
			
		||||
alembic
 | 
			
		||||
{% if cookiecutter.cors_support == 'y' %}wsgicors{% endif %}
 | 
			
		||||
{% if cookiecutter.static_support == 'y' %}whitenoise{% endif %}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
#
 | 
			
		||||
# This file is autogenerated by pip-compile
 | 
			
		||||
# To update, run:
 | 
			
		||||
#
 | 
			
		||||
#    pip-compile --output-file requirements.txt requirements.in
 | 
			
		||||
#
 | 
			
		||||
--trusted-host pypi.python.org
 | 
			
		||||
 | 
			
		||||
click==7.0
 | 
			
		||||
molten==0.7.3
 | 
			
		||||
mypy-extensions==0.4.1    # via typing-inspect
 | 
			
		||||
psycopg2-binary==2.7.6.1
 | 
			
		||||
sqlalchemy==1.2.14
 | 
			
		||||
typing-extensions==3.6.6  # via molten
 | 
			
		||||
typing-inspect==0.3.1     # via molten
 | 
			
		||||
@ -0,0 +1,15 @@
 | 
			
		||||
#!/usr/bin/env bash
 | 
			
		||||
 | 
			
		||||
# setting -e to exit immediately on a command failure.
 | 
			
		||||
# setting -o pipefail sets the exit code of a pipeline to that of the rightmost command to exit with a non-zero status, or to zero if all commands of the pipeline exit successfully.
 | 
			
		||||
set -eo pipefail
 | 
			
		||||
 | 
			
		||||
if [ -z "$VIRTUAL_ENV" ]; then
 | 
			
		||||
    echo "warning: you are not in a virtualenv"
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
pip install -U pip pip-tools
 | 
			
		||||
pip-compile requirements.in
 | 
			
		||||
pip-compile dev_requirements.in
 | 
			
		||||
pip-sync requirements.txt dev_requirements.txt
 | 
			
		||||
@ -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():
 | 
			
		||||
    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(scope="session")
 | 
			
		||||
def app_global():
 | 
			
		||||
    _, app = create_app()
 | 
			
		||||
    yield app
 | 
			
		||||
 | 
			
		||||
    yield
 | 
			
		||||
 | 
			
		||||
    Base.metadata.drop_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)()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@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
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
import logging.config
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
FORMAT = "[%(asctime)s] [PID %(process)d] [%(threadName)s] [%(request_id)s] [%(name)s] [%(levelname)s] %(message)s"  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup_logging():
 | 
			
		||||
    logging.config.dictConfig(
 | 
			
		||||
        {
 | 
			
		||||
            "disable_existing_loggers": False,
 | 
			
		||||
            "version": 1,
 | 
			
		||||
            "filters": {
 | 
			
		||||
                "request_id": {"()": "molten.contrib.request_id.RequestIdFilter"}
 | 
			
		||||
            },
 | 
			
		||||
            "formatters": {"console": {"format": FORMAT}},
 | 
			
		||||
            "handlers": {
 | 
			
		||||
                "default": {
 | 
			
		||||
                    "level": "DEBUG",
 | 
			
		||||
                    "class": "logging.StreamHandler",
 | 
			
		||||
                    "stream": sys.stderr,
 | 
			
		||||
                    "formatter": "console",
 | 
			
		||||
                    "filters": ["request_id"],
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            "loggers": {
 | 
			
		||||
                "": {"handlers": ["default"], "level": "DEBUG", "propagate": False}
 | 
			
		||||
            },
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@ -1,10 +1,26 @@
 | 
			
		||||
[common]
 | 
			
		||||
database_engine_dsn = "postgresql://molten:local@localhost/cookiecutter"
 | 
			
		||||
 | 
			
		||||
[common.wsgicors]
 | 
			
		||||
headers="*"
 | 
			
		||||
methods="*"
 | 
			
		||||
maxage="180"
 | 
			
		||||
origin="*"
 | 
			
		||||
 | 
			
		||||
[common.whitenoise]
 | 
			
		||||
root = "static"
 | 
			
		||||
prefix = "/static"
 | 
			
		||||
autorefresh = true
 | 
			
		||||
 | 
			
		||||
[dev]
 | 
			
		||||
database_engine_params.echo = true
 | 
			
		||||
database_engine_params.connect_args.options = "-c timezone=utc"
 | 
			
		||||
 | 
			
		||||
[test]
 | 
			
		||||
database_engine_dsn = "postgresql://molten:local@localhost/test_cookiecutter"
 | 
			
		||||
database_engine_params.echo = true
 | 
			
		||||
database_engine_params.echo = true
 | 
			
		||||
 | 
			
		||||
[prod.whitenoise]
 | 
			
		||||
root = "static"
 | 
			
		||||
prefix = "/static"
 | 
			
		||||
autorefresh = false
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue