Merge branch 'develop'

master
androiddrew 6 years ago
commit 4a505318d5

@ -1,4 +1,8 @@
import click
from molten.contrib.sqlalchemy import EngineData
from {{cookiecutter.project_slug}}.index import create_app
app = create_app()
@click.group()
@ -15,11 +19,8 @@ def runserver(host, port):
"""
Runs a Werkzueg development server. Do no use for production.
"""
from {{cookiecutter.project_slug}}.index import create_app
from werkzeug.serving import run_simple
app = create_app()
run_simple(
hostname=host, port=port, application=app, use_debugger=True, use_reloader=True
)
@ -30,23 +31,15 @@ def shell():
"""
Enters an interactive shell with an app instance and dependency resolver.
"""
import rlcompleter
import readline
from code import InteractiveConsole
from {{cookiecutter.project_slug}}.index import create_app
app = create_app()
helpers = {"app": app, "resolver": app.injector.get_resolver()}
readline.parse_and_bind("Tab: complete")
readline.parse_and_bind("tab: complete")
interpreter = InteractiveConsole(helpers)
interpreter.interact(
f"""\
Instances in scope: {", ".join(helpers)}.
""",
"",
)
interpreter.interact(f"Instances in scope: {', '.join(helpers)}.", "")
@cli.command()
@ -56,8 +49,11 @@ def initdb():
"""
from {{cookiecutter.project_slug}}.db import Base
def _init(engine_data: EngineData):
Base.metadata.create_all(bind=engine_data.engine)
click.echo("Creating database")
Base.metadata.create_all(bind=engine)
app.injector.get_resolver().resolve(_init)()
click.echo("Database created")
@ -69,10 +65,13 @@ def dropdb():
from {{cookiecutter.project_slug}}.db import Base
def _drop(engine_data: EngineData):
Base.metadata.drop_all(bind=engine_data.engine)
click.echo("Are you sure you would like to drop the database?: [Y/N]")
response = input()
if response.lower() == "y":
Base.metadata.drop_all(bind=engine)
app.injector.get_resolver().resolve(_drop)()
click.echo("Database dropped")
else:
click.echo("Database drop aborted")

@ -0,0 +1,42 @@
import pytest
from molten import testing
from molten.contrib.sqlalchemy import EngineData
from {{cookiecutter.project_slug}}.index import create_app
from {{cookiecutter.project_slug}}.db import Base
# 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)
yield
Base.metadata.drop_all(bind=engine)
@pytest.fixture(scope="function")
def client(app):
"""Creates a testing client"""
return testing.TestClient(app)
@pytest.fixture(scope="function")
def session():
pass

@ -1,12 +1,39 @@
from molten.testing import TestClient
from {{cookiecutter.project_slug}}.index import create_app
def test_welcome_route(client):
message = "welcome to {{cookiecutter.project_slug}}"
response = client.get("/")
content = response.json()
assert message == content.get("message")
app = create_app()
test_client = TestClient(app)
def test_empty_get_todos(client):
response = client.get("/todos")
assert response.status_code == 200
assert response.json() == []
def test_welcome_route():
message = "welcome to {{cookiecutter.project_slug}}"
response = test_client.get("/")
def test_insert_todo(client):
payload = {"todo": "walk the dog"}
response = client.post("/todos", data=payload)
content = response.json()
assert message == content.get("message")
assert response.status_code == 201
assert type(content['id']) == int
assert content['todo'] == payload['todo']
def test_update_todo(client):
payload = {"todo": "sample app"}
response = client.post("/todos", json=payload)
todo = response.json()
print(todo)
update_response = client.patch("{}".format(todo.get("href")), json={"complete": True, "todo": "sample app"})
updated_todo = update_response.json()
print(updated_todo)
assert updated_todo["complete"] == True
def test_todo_not_found(client):
response = client.get("/todo/1111111")
content = response.json()
assert response.status_code == 404
assert content["status"] == 404
assert content["message"]

@ -0,0 +1,2 @@
from .manager import TodoManager, TodoManagerComponent
from .view import todo_routes

@ -0,0 +1,80 @@
from inspect import Parameter
from typing import List
from molten import BaseApp, HTTPError, HTTP_409, HTTP_404
from sqlalchemy.orm import Session
from {{cookiecutter.project_slug}}.manager import BaseManager
from {{cookiecutter.project_slug}}.error import EntityNotFound
from .model import Todo, TodoModel
class TodoManager(BaseManager):
"""A `TodoManager` is accountable for the CRUD operations associated with a `Todo` instance"""
def schema_from_model(self, result: TodoModel) -> Todo:
_todo = Todo(
id=result.id,
href=self.app.reverse_uri("get_todo_by_id", todo_id=result.id),
createdDate=result.created_date,
modifiedDate=result.modified_date,
todo=result.todo,
complete=result.complete
)
return _todo
def model_from_schema(self, todo: Todo) -> TodoModel:
_todo_model = TodoModel(
todo=todo.todo,
complete=todo.complete
)
return _todo_model
def get_todos(self) -> List[Todo]:
"""Retrieves a list of `Todo` representations"""
results = self.session.query(TodoModel).order_by(TodoModel.id).all()
todos = [self.schema_from_model(result) for result in results]
return todos
def get_todo_by_id(self, id) -> Todo:
"""Retrieves a `Todo` representation by id"""
result = self.session.query(TodoModel).filter_by(id=id).one_or_none()
if result is None:
raise EntityNotFound(f"Todo: {id} does not exist")
return self.schema_from_model(result)
def create_todo(self, todo: Todo) -> Todo:
"""Creates a new `Todo` resource and returns its representation"""
todo_model = self.model_from_schema(todo)
self.session.add(todo_model)
self.session.flush()
return self.schema_from_model(todo_model)
def update_todo(self, todo_id: int, todo: Todo) -> Todo:
"""Updates an existing `Todo` resource and returns its new representation"""
result = self.session.query(TodoModel).filter_by(id=todo_id).one_or_none()
if result is None:
raise EntityNotFound(f"Todo: {todo_id} does not exist")
updates = self.model_from_schema(todo)
updates.id = todo_id
self.session.merge(updates)
self.session.flush()
todo = self.schema_from_model(result)
return todo
def delete_todo(self, id):
"""Deletes a `Todo` """
result = self.session.query(TodoModel).filter_by(id=id).one_or_none()
if result is not None:
self.session.delete(result)
return
class TodoManagerComponent:
is_cacheable = True
is_singleton = False
def can_handle_parameter(self, parameter: Parameter) -> bool:
return parameter.annotation is TodoManager
def resolve(self, session: Session, app: BaseApp) -> TodoManager: # type: ignore
return TodoManager(session, app)

@ -0,0 +1,21 @@
from typing import Optional
from molten import schema, field
from sqlalchemy import Column, Text, Boolean
from {{cookiecutter.project_slug}}.db import Base, DBMixin
from {{cookiecutter.project_slug}}.schema import Link
@schema
class Todo:
id: int = field(response_only=True)
createdDate: str = field(response_only=True)
modifiedDate: str = field(response_only=True)
todo: str
complete: Optional[bool]
href: Link = field(response_only=True)
class TodoModel(Base, DBMixin):
__tablename__ = "todo"
todo = Column(Text)
complete = Column(Boolean, default=False)

@ -0,0 +1,49 @@
from typing import List
from molten import Route, Include, HTTP_201, HTTP_202, HTTPError, HTTP_404
from {{cookiecutter.project_slug}}.schema import APIResponse
from {{cookiecutter.project_slug}}.error import EntityNotFound
from .model import Todo
from .manager import TodoManager
def list_todos(todo_manager: TodoManager) -> List[Todo]:
return todo_manager.get_todos()
def create_todo(todo: Todo, todo_manager: TodoManager) -> Todo:
_todo = todo_manager.create_todo(todo)
headers = {"Location": _todo.href}
return HTTP_201, _todo, headers
def delete_todo(todo_id: int, todo_manager: TodoManager):
todo_manager.delete_todo(todo_id)
return (
HTTP_202,
APIResponse(status=202, message=f"Delete request for store: {todo_id} accepted"),
)
def get_todo_by_id(todo_id: int, todo_manager: TodoManager) -> Todo:
try:
_todo = todo_manager.get_todo_by_id(todo_id)
except EntityNotFound as err:
raise HTTPError(HTTP_404,
APIResponse(status=404,
message=err.message)
)
return _todo
def update_todo(todo_id: int, todo: Todo, todo_manager: TodoManager) -> Todo:
return todo_manager.update_todo(todo_id, todo)
todo_routes = Include("/todos", [
Route("", list_todos, method="GET"),
Route("", create_todo, method="POST"),
Route("/{todo_id}", delete_todo, method="DELETE"),
Route("/{todo_id}", get_todo_by_id, method="GET"),
Route("/{todo_id}", update_todo, method="PATCH")
])

@ -0,0 +1,32 @@
from sqlalchemy import Column, BigInteger, DateTime
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.schema import CreateColumn
from sqlalchemy.sql import expression
from sqlalchemy.types import DateTime as DatetimeType
Base = declarative_base()
class utcnow(expression.FunctionElement):
type = DatetimeType()
@compiles(utcnow, "postgresql")
def pg_utcnow(element, compiler, **kw):
return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
@compiles(CreateColumn, 'postgresql')
def use_identity(element, compiler, **kw):
text = compiler.visit_create_column(element, **kw)
text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY")
return text
class DBMixin:
id = Column(BigInteger, primary_key=True)
created_date = Column(DateTime(timezone=True), server_default=utcnow())
modified_date = Column(
DateTime(timezone=True), server_default=utcnow(), onupdate=utcnow()
)

@ -0,0 +1,5 @@
from molten.errors import MoltenError
class EntityNotFound(MoltenError):
"""Raised when an entity is not found using an `exists` check in sqlalchemy."""

@ -1,24 +1,39 @@
import os
from typing import Tuple
from molten import App, Route, ResponseRendererMiddleware
from molten.http import HTTP_404, Request
from molten.settings import Settings, SettingsComponent
from molten.contrib.sqlalchemy import SQLAlchemyMiddleware
from molten.contrib.sqlalchemy import SQLAlchemyMiddleware, SQLAlchemyEngineComponent, SQLAlchemySessionComponent
from .api.welcome import welcome
from .api.todo import TodoManagerComponent, todo_routes
from .common import ExtJSONRenderer
from .schema import APIResponse
settings = Settings({})
settings = Settings(
{
"database_engine_dsn": os.getenv(
"DATABASE_DSN", "postgresql://molten:local@localhost/cookiecutter"
),
"database_engine_params": {
"echo": True,
"connect_args": {"options": "-c timezone=utc"},
},
}
)
components = [
SettingsComponent(settings),
SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(),
TodoManagerComponent(),
]
middleware = [ResponseRendererMiddleware(), SQLAlchemyMiddleware()]
renderers = [ExtJSONRenderer()]
routes = [Route("/", welcome, "GET")]
routes = [Route("/", welcome, "GET")] + [todo_routes]
class ExtApp(App):

@ -0,0 +1,32 @@
from abc import ABCMeta, abstractmethod
from molten import BaseApp, HTTPError, HTTP_409, HTTP_404
from sqlalchemy.orm import Session
class BaseManager(metaclass=ABCMeta):
"""Base instance for Model managers"""
def __init__(self, session: Session, app: BaseApp):
self.session = session
self.app = app
@abstractmethod
def model_from_schema(self, schema):
"""Converts a Schema instance into a SQLAlchemy ORM model instance"""
pass
@abstractmethod
def schema_from_model(self, result):
"""Converts a SQLAlchemy results proxy into a Schema instance"""
pass
def raise_409(self, id:int):
"""Raises a 409 HTTP error response in the event of Conflict"""
raise HTTPError(
HTTP_409,
{
"status": 409,
"message": f"Entity {self.__class__.__name__} with id: {id} already exists"
}
)
Loading…
Cancel
Save