Saving my work

master
Drew Bednar 5 years ago
parent 59a05ca503
commit 262d32fea4

@ -0,0 +1,76 @@
from inspect import Parameter
from typing import List
from molten import BaseApp, HTTPError, HTTP_409, HTTP_404
from sqlalchemy.orm import Session
from casbin_api.manager import BaseManager
from casbin_api.error import EntityNotFound
from .model import User, UserModel
class UserManager(BaseManager):
"""A `TodoManager` is accountable for the CRUD operations associated with a `Todo` instance"""
def schema_from_model(self, result: UserModel) -> User:
_user = User(
id=result.id,
href=self.app.reverse_uri("get_user_by_id", user_id=result.id),
createdDate=result.created_date,
modifiedDate=result.modified_date,
todo=result.todo,
)
return _user
def model_from_schema(self, user: User) -> UserModel:
_user_model = UserModel(user.email, user.passwd)
return _user_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,41 @@
import bcrypt
from typing import Optional
from molten import schema, field
from sqlalchemy import relationship, Column, Text, String, Boolean
from casbin_api.db import Base, DBMixin
from casbin_api.schema import Link
BCRYPT_ROUNDS = 11
@schema
class AuthToken:
token: str = field(response_only=True)
@schema
class User:
id: int = field(response_only=True)
createdDate: str = field(response_only=True)
modifiedDate: str = field(response_only=True)
email: str
passwd: str = field(request_only=True)
href: Link = field(response_only=True)
class UserModel(Base, DBMixin):
__tablename__ = "user_account"
email = Column(Text)
passwd = Column(String(255))
todos = relationship("TodoModel", back_populates="user")
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
def __init__(self, email, passwd, is_admin=False):
self.email = email
self.passwd = bcrypt.hashpw(passwd.encode('utf-8', bcrypt.gensalt(BCRYPT_ROUNDS))).decode('utf-8')
self.is_admin = is_admin
def check_password(self, passwd):
"""Checks provided password against saved password hash."""
return bcrypt.checkpw(passwd.encode('utf-8'), self.passwd.encode('utf-8'))

@ -0,0 +1,41 @@
from typing import List
from molten import Route, Include, HTTP_201, HTTP_202, HTTPError, HTTP_404
from casbin_api.schema import APIResponse
from casbin_api.error import EntityNotFound
from .model import User, AuthToken
from .manager import UserManager
def create_user(user: User, user_manager: UserManager) -> User:
_user = user_manager.create_user(user)
headers = {"Location": _user.href}
return HTTP_201, _user, headers
def get_user_by_id(user_id: int, user_manager: UserManager) -> User:
try:
_user = user_manager.get_user_by_id(user_id)
except EntityNotFound as err:
raise HTTPError(HTTP_404, APIResponse(status=404, message=err.message))
return _user
# Update user only if you are the user or you are admin
def update_user(user_id: int, user: User, user_manager: UserManager) -> User:
return user_manager.update_todo(user_id, user)
def login_user(user: User, user_manager: UserManager) -> AuthToken:
pass
auth_routes = Include(
"/auth",
[
Route("/login", login_user, method="POST"),
Route("/register", create_user, method="POST"),
Route("/profile/{user_id}", get_user_by_id, method="GET"),
Route("/profile/{user_id}", update_user, method="PUT"),
],
)

@ -18,15 +18,12 @@ class TodoManager(BaseManager):
createdDate=result.created_date, createdDate=result.created_date,
modifiedDate=result.modified_date, modifiedDate=result.modified_date,
todo=result.todo, todo=result.todo,
complete=result.complete complete=result.complete,
) )
return _todo return _todo
def model_from_schema(self, todo: Todo) -> TodoModel: def model_from_schema(self, todo: Todo) -> TodoModel:
_todo_model = TodoModel( _todo_model = TodoModel(todo=todo.todo, complete=todo.complete)
todo=todo.todo,
complete=todo.complete
)
return _todo_model return _todo_model
def get_todos(self) -> List[Todo]: def get_todos(self) -> List[Todo]:

@ -29,10 +29,7 @@ def get_todo_by_id(todo_id: int, todo_manager: TodoManager) -> Todo:
try: try:
_todo = todo_manager.get_todo_by_id(todo_id) _todo = todo_manager.get_todo_by_id(todo_id)
except EntityNotFound as err: except EntityNotFound as err:
raise HTTPError(HTTP_404, raise HTTPError(HTTP_404, APIResponse(status=404, message=err.message))
APIResponse(status=404,
message=err.message)
)
return _todo return _todo
@ -40,10 +37,13 @@ def update_todo(todo_id: int, todo: Todo, todo_manager: TodoManager) -> Todo:
return todo_manager.update_todo(todo_id, todo) return todo_manager.update_todo(todo_id, todo)
todo_routes = Include("/todos", [ todo_routes = Include(
Route("", list_todos, method="GET"), "/todos",
Route("", create_todo, method="POST"), [
Route("/{todo_id}", delete_todo, method="DELETE"), Route("", list_todos, method="GET"),
Route("/{todo_id}", get_todo_by_id, method="GET"), Route("", create_todo, method="POST"),
Route("/{todo_id}", update_todo, method="PATCH") Route("/{todo_id}", delete_todo, method="DELETE"),
]) Route("/{todo_id}", get_todo_by_id, method="GET"),
Route("/{todo_id}", update_todo, method="PATCH"),
],
)

@ -17,7 +17,7 @@ def pg_utcnow(element, compiler, **kw):
return "TIMEZONE('utc', CURRENT_TIMESTAMP)" return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
@compiles(CreateColumn, 'postgresql') @compiles(CreateColumn, "postgresql")
def use_identity(element, compiler, **kw): def use_identity(element, compiler, **kw):
text = compiler.visit_create_column(element, **kw) text = compiler.visit_create_column(element, **kw)
text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY") text = text.replace("SERIAL", "INT GENERATED BY DEFAULT AS IDENTITY")

@ -3,9 +3,20 @@ from molten import App, Route, ResponseRendererMiddleware, Settings
from molten.http import HTTP_404, Request 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,
)
from wsgicors import CORS from wsgicors import CORS
from whitenoise import WhiteNoise from whitenoise import WhiteNoise
from molten_jwt import (
JWT,
JWTIdentity,
JWTComponent,
JWTIdentityComponent,
JWTAuthMiddleware,
)
from .api.welcome import welcome from .api.welcome import welcome
from .api.todo import TodoManagerComponent, todo_routes from .api.todo import TodoManagerComponent, todo_routes
@ -18,28 +29,29 @@ get_schema = OpenAPIHandler(
metadata=Metadata( metadata=Metadata(
title="casbin_api", title="casbin_api",
description="A Molten web api with RBAC provided by pycasbin", description="A Molten web api with RBAC provided by pycasbin",
version="0.0.0" version="0.0.0",
) )
) )
get_docs = OpenAPIUIHandler() get_docs = OpenAPIUIHandler()
components = [ components = [
JWTIdentityComponent(),
SettingsComponent(settings), SettingsComponent(settings),
SQLAlchemyEngineComponent(), SQLAlchemyEngineComponent(),
SQLAlchemySessionComponent(), SQLAlchemySessionComponent(),
TodoManagerComponent(), TodoManagerComponent(),
] ]
middleware = [ResponseRendererMiddleware(), SQLAlchemyMiddleware()] middleware = [ResponseRendererMiddleware(), SQLAlchemyMiddleware(), JWTAuthMiddleware]
renderers = [ExtJSONRenderer()] renderers = [ExtJSONRenderer()]
routes = [ routes = [
Route("/", welcome, "GET"), Route("/", welcome, "GET"),
Route("/_schema", get_schema, "GET"), Route("/_schema", get_schema, "GET"),
Route("/_docs", get_docs, "GET"), Route("/_docs", get_docs, "GET"),
] + [todo_routes] ] + [todo_routes]
class ExtApp(App): class ExtApp(App):
@ -74,9 +86,9 @@ def create_app(_components=None, _middleware=None, _routes=None, _renderers=None
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,
) )
wrapped_app = CORS(wrapped_app, **settings.strict_get("wsgicors")) wrapped_app = CORS(wrapped_app, **settings.strict_get("wsgicors"))
wrapped_app = WhiteNoise(wrapped_app, **settings.strict_get("whitenoise")) wrapped_app = WhiteNoise(wrapped_app, **settings.strict_get("whitenoise"))
return wrapped_app, app return wrapped_app, app

@ -20,13 +20,12 @@ class BaseManager(metaclass=ABCMeta):
"""Converts a SQLAlchemy results proxy into a Schema instance""" """Converts a SQLAlchemy results proxy into a Schema instance"""
pass pass
def raise_409(self, id:int): def raise_409(self, id: int):
"""Raises a 409 HTTP error response in the event of Conflict""" """Raises a 409 HTTP error response in the event of Conflict"""
raise HTTPError( raise HTTPError(
HTTP_409, HTTP_409,
{ {
"status": 409, "status": 409,
"message": f"Entity {self.__class__.__name__} with id: {id} already exists" "message": f"Entity {self.__class__.__name__} with id: {id} already exists",
} },
) )

@ -47,7 +47,9 @@ def initdb():
""" """
Initialize database Initialize database
""" """
click.echo("This feature has been commented out. Please use alembic to manage your database initialization and changes.") click.echo(
"This feature has been commented out. Please use alembic to manage your database initialization and changes."
)
# from casbin_api.db import Base # from casbin_api.db import Base
# #
# def _init(engine_data: EngineData): # def _init(engine_data: EngineData):

@ -1,7 +1,9 @@
"""isort:skip_file """isort:skip_file
""" """
import os import os
import sys; sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) # noqa import sys
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..")) # noqa
from alembic import context from alembic import context
from casbin_api.index import create_app from casbin_api.index import create_app

@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = '697808db5f01' revision = "697808db5f01"
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,18 +18,29 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('todo', op.create_table(
sa.Column('id', sa.BigInteger(), nullable=False), "todo",
sa.Column('created_date', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), nullable=True), sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column('modified_date', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"), nullable=True), sa.Column(
sa.Column('todo', sa.Text(), nullable=True), "created_date",
sa.Column('complete', sa.Boolean(), nullable=True), sa.DateTime(timezone=True),
sa.PrimaryKeyConstraint('id') server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"),
nullable=True,
),
sa.Column(
"modified_date",
sa.DateTime(timezone=True),
server_default=sa.text("TIMEZONE('utc', CURRENT_TIMESTAMP)"),
nullable=True,
),
sa.Column("todo", sa.Text(), nullable=True),
sa.Column("complete", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
) )
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('todo') op.drop_table("todo")
# ### end Alembic commands ### # ### end Alembic commands ###

@ -1,3 +1,4 @@
bcrypt
click click
molten molten
sqlalchemy sqlalchemy

@ -6,7 +6,8 @@
# #
alembic==1.4.0 alembic==1.4.0
authlib==0.12.1 # via molten-jwt authlib==0.12.1 # via molten-jwt
cffi==1.14.0 # via cryptography bcrypt==3.1.7
cffi==1.14.0 # via bcrypt, cryptography
click==7.0 click==7.0
cryptography==2.8 # via authlib cryptography==2.8 # via authlib
mako==1.1.1 # via alembic mako==1.1.1 # via alembic
@ -18,7 +19,7 @@ psycopg2-binary==2.8.4
pycparser==2.19 # via cffi pycparser==2.19 # via cffi
python-dateutil==2.8.1 # via alembic python-dateutil==2.8.1 # via alembic
python-editor==1.0.4 # via alembic python-editor==1.0.4 # via alembic
six==1.14.0 # via cryptography, python-dateutil six==1.14.0 # via bcrypt, cryptography, python-dateutil
sqlalchemy==1.3.13 sqlalchemy==1.3.13
typing-extensions==3.7.4.1 # via molten, typing-inspect typing-extensions==3.7.4.1 # via molten, typing-inspect
typing-inspect==0.5.0 # via molten typing-inspect==0.5.0 # via molten

@ -7,12 +7,14 @@ from casbin_api.index import create_app
def truncate_all_tables(session: Session): def truncate_all_tables(session: Session):
table_names = session.execute(""" table_names = session.execute(
"""
select table_name from information_schema.tables select table_name from information_schema.tables
where table_schema = 'public' where table_schema = 'public'
and table_type = 'BASE TABLE' and table_type = 'BASE TABLE'
and table_name != 'alembic_version' and table_name != 'alembic_version'
""") """
)
for (table_name,) in table_names: for (table_name,) in table_names:
# "truncate" can deadlock so we use delete which is guaranteed not to. # "truncate" can deadlock so we use delete which is guaranteed not to.
session.execute(f"delete from {table_name}") session.execute(f"delete from {table_name}")
@ -47,5 +49,7 @@ def load_component(app):
def load(annotation): def load(annotation):
def loader(c: annotation): def loader(c: annotation):
return c return c
return app.injector.get_resolver().resolve(loader)() return app.injector.get_resolver().resolve(loader)()
return load return load

@ -16,8 +16,8 @@ def test_insert_todo(client):
response = client.post("/todos", data=payload) response = client.post("/todos", data=payload)
content = response.json() content = response.json()
assert response.status_code == 201 assert response.status_code == 201
assert type(content['id']) == int assert type(content["id"]) == int
assert content['todo'] == payload['todo'] assert content["todo"] == payload["todo"]
def test_get_individual_todo_by_href(client): def test_get_individual_todo_by_href(client):
@ -34,7 +34,9 @@ def test_update_todo(client):
payload = {"todo": "sample app"} payload = {"todo": "sample app"}
response = client.post("/todos", json=payload) response = client.post("/todos", json=payload)
todo = response.json() todo = response.json()
update_response = client.patch("{}".format(todo.get("href")), json={"complete": True, "todo": "sample app"}) update_response = client.patch(
"{}".format(todo.get("href")), json={"complete": True, "todo": "sample app"}
)
updated_todo = update_response.json() updated_todo = update_response.json()
assert updated_todo["complete"] == True assert updated_todo["complete"] == True

@ -0,0 +1,57 @@
def test_token_route(client):
message = "welcome to casbin_api"
response = client.get("/")
content = response.json()
assert message == content.get("message")
def test_empty_get_todos(client):
response = client.get("/todos")
assert response.status_code == 200
assert response.json() == []
def test_insert_todo(client):
payload = {"todo": "walk the dog"}
response = client.post("/todos", data=payload)
content = response.json()
assert response.status_code == 201
assert type(content["id"]) == int
assert content["todo"] == payload["todo"]
def test_get_individual_todo_by_href(client):
payload = {"todo": "my individual todo"}
response = client.post("/todos", data=payload)
content = response.json()
get_response = client.get(f"{content.get('href')}")
get_content = get_response.json()
assert get_response.status_code == 200
assert content == get_content
def test_update_todo(client):
payload = {"todo": "sample app"}
response = client.post("/todos", json=payload)
todo = response.json()
update_response = client.patch(
"{}".format(todo.get("href")), json={"complete": True, "todo": "sample app"}
)
updated_todo = update_response.json()
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"]
def test_delete_todo(client):
payload = {"todo": "sample app"}
response = client.post("/todos", json=payload)
todo = response.json()
delete_response = client.delete(f"/todos/{todo.get('id')}")
assert delete_response.status_code == 202

@ -11,5 +11,5 @@ def test_extended_encoder_date_parsing():
def test_extended_encoder_decimal_casting(): def test_extended_encoder_decimal_casting():
json_renderer = ExtJSONRenderer() json_renderer = ExtJSONRenderer()
test_decimal = Decimal('1.0') test_decimal = Decimal("1.0")
assert 1.0 == json_renderer.default(test_decimal) assert 1.0 == json_renderer.default(test_decimal)

Loading…
Cancel
Save