Merge branch 'develop'
commit
0936ce52ae
@ -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/
|
@ -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
|
click
|
||||||
molten
|
molten
|
||||||
sqlalchemy
|
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
|
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")
|
|
||||||
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)()
|
@pytest.fixture(scope="session")
|
||||||
|
def app_global():
|
||||||
Base.metadata.create_all(bind=engine)
|
_, 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):
|
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
|
||||||
|
@ -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]
|
[common]
|
||||||
database_engine_dsn = "postgresql://molten:local@localhost/cookiecutter"
|
database_engine_dsn = "postgresql://molten:local@localhost/cookiecutter"
|
||||||
|
|
||||||
|
[common.wsgicors]
|
||||||
|
headers="*"
|
||||||
|
methods="*"
|
||||||
|
maxage="180"
|
||||||
|
origin="*"
|
||||||
|
|
||||||
|
[common.whitenoise]
|
||||||
|
root = "static"
|
||||||
|
prefix = "/static"
|
||||||
|
autorefresh = true
|
||||||
|
|
||||||
[dev]
|
[dev]
|
||||||
database_engine_params.echo = true
|
database_engine_params.echo = true
|
||||||
database_engine_params.connect_args.options = "-c timezone=utc"
|
database_engine_params.connect_args.options = "-c timezone=utc"
|
||||||
|
|
||||||
[test]
|
[test]
|
||||||
database_engine_dsn = "postgresql://molten:local@localhost/test_cookiecutter"
|
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