Merge branch 'develop'

master
androiddrew 6 years ago
commit 4a505318d5

@ -1,4 +1,8 @@
import click import click
from molten.contrib.sqlalchemy import EngineData
from {{cookiecutter.project_slug}}.index import create_app
app = create_app()
@click.group() @click.group()
@ -15,11 +19,8 @@ def runserver(host, port):
""" """
Runs a Werkzueg development server. Do no use for production. Runs a Werkzueg development server. Do no use for production.
""" """
from {{cookiecutter.project_slug}}.index import create_app
from werkzeug.serving import run_simple from werkzeug.serving import run_simple
app = create_app()
run_simple( run_simple(
hostname=host, port=port, application=app, use_debugger=True, use_reloader=True 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. Enters an interactive shell with an app instance and dependency resolver.
""" """
import rlcompleter
import readline import readline
from code import InteractiveConsole from code import InteractiveConsole
from {{cookiecutter.project_slug}}.index import create_app
app = create_app()
helpers = {"app": app, "resolver": app.injector.get_resolver()} helpers = {"app": app, "resolver": app.injector.get_resolver()}
readline.parse_and_bind("Tab: complete") readline.parse_and_bind("tab: complete")
interpreter = InteractiveConsole(helpers) interpreter = InteractiveConsole(helpers)
interpreter.interact( interpreter.interact(f"Instances in scope: {', '.join(helpers)}.", "")
f"""\
Instances in scope: {", ".join(helpers)}.
""",
"",
)
@cli.command() @cli.command()
@ -56,8 +49,11 @@ def initdb():
""" """
from {{cookiecutter.project_slug}}.db import Base from {{cookiecutter.project_slug}}.db import Base
def _init(engine_data: EngineData):
Base.metadata.create_all(bind=engine_data.engine)
click.echo("Creating database") click.echo("Creating database")
Base.metadata.create_all(bind=engine) app.injector.get_resolver().resolve(_init)()
click.echo("Database created") click.echo("Database created")
@ -69,10 +65,13 @@ def dropdb():
from {{cookiecutter.project_slug}}.db import Base 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]") click.echo("Are you sure you would like to drop the database?: [Y/N]")
response = input() response = input()
if response.lower() == "y": if response.lower() == "y":
Base.metadata.drop_all(bind=engine) app.injector.get_resolver().resolve(_drop)()
click.echo("Database dropped") click.echo("Database dropped")
else: else:
click.echo("Database drop aborted") 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 def test_welcome_route(client):
from {{cookiecutter.project_slug}}.index import create_app 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}}" def test_insert_todo(client):
response = test_client.get("/") payload = {"todo": "walk the dog"}
response = client.post("/todos", data=payload)
content = response.json() 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 typing import Tuple
from molten import App, Route, ResponseRendererMiddleware from molten import App, Route, ResponseRendererMiddleware
from molten.http import HTTP_404, Request from molten.http import HTTP_404, Request
from molten.settings import Settings, SettingsComponent 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.welcome import welcome
from .api.todo import TodoManagerComponent, todo_routes
from .common import ExtJSONRenderer from .common import ExtJSONRenderer
from .schema import APIResponse 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 = [ components = [
SettingsComponent(settings), SettingsComponent(settings),
SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(),
TodoManagerComponent(),
] ]
middleware = [ResponseRendererMiddleware(), SQLAlchemyMiddleware()] middleware = [ResponseRendererMiddleware(), SQLAlchemyMiddleware()]
renderers = [ExtJSONRenderer()] renderers = [ExtJSONRenderer()]
routes = [Route("/", welcome, "GET")] routes = [Route("/", welcome, "GET")] + [todo_routes]
class ExtApp(App): 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