Merge branch 'develop'

master
androiddrew 6 years ago
commit 0936ce52ae

5
.gitignore vendored

@ -59,4 +59,7 @@ docs/_build/
target/ target/
# Pycharm # 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> $ cd <app_dir>
$ python -m venv env $ python -m venv env
$ source env/bin/activate $ 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 #### Run tests
``` ```
$ export ENVIRONMENT=test && pytest -v $ env ENVIRONMENT=test pytest -v
``` ```
#### Using the management script #### Using the management script
@ -44,8 +45,12 @@ MIT Licensed.
## Changelog ## Changelog
### 0.3.0 (01/27/2019)
- Added migrations.
- Changed testing fixtures.
### 0.2.0 (01/15/2019) ### 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) ### 0.1.0 (12/09/2018)
- Initial release - Initial release.

@ -5,5 +5,7 @@
"project_name": "molten-app", "project_name": "molten-app",
"project_slug": "{{ cookiecutter.project_name.lower().strip().replace(' ', '_').replace('-','_')}}", "project_slug": "{{ cookiecutter.project_name.lower().strip().replace(' ', '_').replace('-','_')}}",
"description": "An molten project templated by cookiecutter-molten", "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"] "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.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 molten.contrib.sqlalchemy import EngineData
from {{cookiecutter.project_slug}}.index import create_app from {{cookiecutter.project_slug}}.index import create_app
app = create_app() _, app = create_app()
@click.group() @click.group()
@ -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.

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

@ -4,12 +4,19 @@ 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' %}
from wsgicors import CORS
{%- endif %}
{%- if cookiecutter.static_support == 'y' %}
from whitenoise import WhiteNoise
{%- 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 .schema import APIResponse from .schema import APIResponse
from .settings import SETTINGS from . import settings
get_schema = OpenAPIHandler( get_schema = OpenAPIHandler(
metadata=Metadata( metadata=Metadata(
@ -22,7 +29,7 @@ get_schema = OpenAPIHandler(
get_docs = OpenAPIUIHandler() get_docs = OpenAPIUIHandler()
components = [ components = [
SettingsComponent(SETTINGS), SettingsComponent(settings),
SQLAlchemyEngineComponent(), SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(), SQLAlchemySessionComponent(),
TodoManagerComponent(), TodoManagerComponent(),
@ -63,12 +70,23 @@ class ExtApp(App):
def create_app(_components=None, _middleware=None, _routes=None, _renderers=None): 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, components=_components or components,
middleware=_middleware or middleware, middleware=_middleware or middleware,
routes=_routes or routes, routes=_routes or routes,
renderers=_renderers or renderers 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] [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…
Cancel
Save