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"),
],
)

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

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

@ -29,10 +29,7 @@ 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)
)
raise HTTPError(HTTP_404, APIResponse(status=404, message=err.message))
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)
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")
])
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"),
],
)

@ -1 +1 @@
from .views import welcome
from .views import welcome

@ -17,7 +17,7 @@ def pg_utcnow(element, compiler, **kw):
return "TIMEZONE('utc', CURRENT_TIMESTAMP)"
@compiles(CreateColumn, 'postgresql')
@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")

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

@ -20,13 +20,12 @@ class BaseManager(metaclass=ABCMeta):
"""Converts a SQLAlchemy results proxy into a Schema instance"""
pass
def raise_409(self, id:int):
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"
}
"message": f"Entity {self.__class__.__name__} with id: {id} already exists",
},
)

@ -47,7 +47,9 @@ def initdb():
"""
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
#
# def _init(engine_data: EngineData):

@ -1,7 +1,9 @@
"""isort:skip_file
"""
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 casbin_api.index import create_app
@ -18,4 +20,4 @@ def run_migrations_online(engine_data: EngineData):
context.run_migrations()
app.injector.get_resolver().resolve(run_migrations_online)()
app.injector.get_resolver().resolve(run_migrations_online)()

@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '697808db5f01'
revision = "697808db5f01"
down_revision = None
branch_labels = None
depends_on = None
@ -18,18 +18,29 @@ depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('todo',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('created_date', sa.DateTime(timezone=True), 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')
op.create_table(
"todo",
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column(
"created_date",
sa.DateTime(timezone=True),
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 ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('todo')
op.drop_table("todo")
# ### end Alembic commands ###

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

@ -6,7 +6,8 @@
#
alembic==1.4.0
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
cryptography==2.8 # via authlib
mako==1.1.1 # via alembic
@ -18,7 +19,7 @@ psycopg2-binary==2.8.4
pycparser==2.19 # via cffi
python-dateutil==2.8.1 # 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
typing-extensions==3.7.4.1 # via molten, typing-inspect
typing-inspect==0.5.0 # via molten

@ -7,12 +7,14 @@ from casbin_api.index import create_app
def truncate_all_tables(session: Session):
table_names = session.execute("""
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}")
@ -47,5 +49,7 @@ def load_component(app):
def load(annotation):
def loader(c: annotation):
return c
return app.injector.get_resolver().resolve(loader)()
return load

@ -16,8 +16,8 @@ def test_insert_todo(client):
response = client.post("/todos", data=payload)
content = response.json()
assert response.status_code == 201
assert type(content['id']) == int
assert content['todo'] == payload['todo']
assert type(content["id"]) == int
assert content["todo"] == payload["todo"]
def test_get_individual_todo_by_href(client):
@ -34,7 +34,9 @@ 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"})
update_response = client.patch(
"{}".format(todo.get("href")), json={"complete": True, "todo": "sample app"}
)
updated_todo = update_response.json()
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():
json_renderer = ExtJSONRenderer()
test_decimal = Decimal('1.0')
test_decimal = Decimal("1.0")
assert 1.0 == json_renderer.default(test_decimal)

Loading…
Cancel
Save