Merge branch 'develop'

master
androiddrew 6 years ago
commit 0936ce52ae

5
.gitignore vendored

@ -59,4 +59,7 @@ docs/_build/
target/
# Pycharm
.idea
.idea
# Mac
.DS_Store

@ -15,12 +15,13 @@ You will be asked for some basic information regarding your project (name, proje
$ cd <app_dir>
$ python -m venv env
$ source env/bin/activate
$ pip install -r dev_requirements.txt
$ pip-sync dev_requirements.txt requirements.txt
```
Follow the initial setup steps included in the project.
#### Run tests
```
$ export ENVIRONMENT=test && pytest -v
$ env ENVIRONMENT=test pytest -v
```
#### Using the management script
@ -44,8 +45,12 @@ MIT Licensed.
## Changelog
### 0.3.0 (01/27/2019)
- Added migrations.
- Changed testing fixtures.
### 0.2.0 (01/15/2019)
- Added TOML settings file and .coveragerc file
- Added TOML settings file and .coveragerc file.
### 0.1.0 (12/09/2018)
- Initial release
- Initial release.

@ -5,5 +5,7 @@
"project_name": "molten-app",
"project_slug": "{{ cookiecutter.project_name.lower().strip().replace(' ', '_').replace('-','_')}}",
"description": "An molten project templated by cookiecutter-molten",
"cors_support": "n",
"static_support": "n",
"open_source_license": ["MIT license", "BSD license", "ISC license", "Apache Software License 2.0", "GNU General Public License v3", "Not open source"]
}

@ -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

@ -2,7 +2,7 @@ import click
from molten.contrib.sqlalchemy import EngineData
from {{cookiecutter.project_slug}}.index import create_app
app = create_app()
_, app = create_app()
@click.group()
@ -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.

@ -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

@ -4,12 +4,19 @@ 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' %}
from wsgicors import CORS
{%- endif %}
{%- if cookiecutter.static_support == 'y' %}
from whitenoise import WhiteNoise
{%- 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(
@ -22,7 +29,7 @@ get_schema = OpenAPIHandler(
get_docs = OpenAPIUIHandler()
components = [
SettingsComponent(SETTINGS),
SettingsComponent(settings),
SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(),
TodoManagerComponent(),
@ -63,12 +70,23 @@ class ExtApp(App):
def create_app(_components=None, _middleware=None, _routes=None, _renderers=None):
"""
Factory function for the creation of a `molten.App` instance
Factory function for the creation of a `molten.App`.
"""
app = ExtApp(
setup_logging()
wrapped_app = app = ExtApp(
components=_components or components,
middleware=_middleware or middleware,
routes=_routes or routes,
renderers=_renderers or renderers
)
return app
{%- if cookiecutter.cors_support == 'y' %}
wrapped_app = CORS(wrapped_app, **settings.strict_get("wsgicors"))
{%- endif %}
{%- if cookiecutter.static_support == 'y' %}
wrapped_app = WhiteNoise(wrapped_app, **settings.strict_get("whitenoise"))
{%- endif %}
return wrapped_app, app

@ -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…
Cancel
Save