From d0a9b556c62bff401f973af62a55867073d9552f Mon Sep 17 00:00:00 2001 From: androiddrew Date: Fri, 27 Apr 2018 21:40:04 -0400 Subject: [PATCH] leveled up, minus auth --- cookie_api/__init__.py | 4 +- cookie_api/app.py | 75 ++++++++----------- cookie_api/auth.py | 16 ++-- cookie_api/commands.py | 16 ---- cookie_api/{logging.py => logger.py} | 1 - cookie_api/renders.py | 25 ------- cookie_api/schema.py | 61 ++++----------- cookie_api/types.py | 25 ------- cookie_api/util.py | 106 +++++++++++++++++++++++++++ requirements.txt | 67 +++-------------- wsgi.py | 9 ++- 11 files changed, 172 insertions(+), 233 deletions(-) delete mode 100644 cookie_api/commands.py rename cookie_api/{logging.py => logger.py} (99%) delete mode 100644 cookie_api/renders.py delete mode 100644 cookie_api/types.py create mode 100644 cookie_api/util.py diff --git a/cookie_api/__init__.py b/cookie_api/__init__.py index c62c7e5..c1a1dd8 100644 --- a/cookie_api/__init__.py +++ b/cookie_api/__init__.py @@ -1,3 +1 @@ -from cookie_api.renders import JSONRenderer -from cookie_api.app import application_factory -from cookie_api.models import Base \ No newline at end of file +from cookie_api.app import application_factory \ No newline at end of file diff --git a/cookie_api/app.py b/cookie_api/app.py index cea3042..4f5923c 100644 --- a/cookie_api/app.py +++ b/cookie_api/app.py @@ -1,61 +1,40 @@ import typing -from apistar import Include, Route, http, annotate -from apistar.backends import sqlalchemy_backend -from apistar.backends.sqlalchemy_backend import Session -from apistar.frameworks.wsgi import WSGIApp as App -from apistar.handlers import docs_urls, static_urls -from apistar.interfaces import Router, Injector, Auth -from apistar_jwt.authentication import JWTAuthentication -from apistar_alembic_migrations import commands as migrate_commands -from apistar_mail import mail_component +from apistar import Route, http, App +from apistar_jwt import JWT import logbook +from sqlalchemy import create_engine - -from cookie_api.auth import auth_routes, auth_components -from cookie_api.commands import commands +from cookie_api.logger import global_init from cookie_api.models import Cookie -#from cookie_api.schema import CookieSchema from cookie_api.schema import CookieSchema -from cookie_api import logging +from cookie_api.util import SQLAlchemyHook, SQLAlchemySession, Session, ExtJSONResponse -cookie_schema = CookieSchema() +engine = create_engine('postgresql://apistar@localhost:5432/apistar') logger = logbook.Logger('Cookies') -@annotate(authentication=[JWTAuthentication()]) -def get_state(injector: Injector, auth: Auth): - state = injector.state - d = dict() - for k, v in state.items(): - d[k] = str(v) - return d - - def get_cookies(session: Session) -> typing.List[CookieSchema]: - logger.info("Accessing the Cookies resource") cookies = session.query(Cookie).all() - return [CookieSchema(cookie) for cookie in cookies] + return ExtJSONResponse([CookieSchema(cookie) for cookie in cookies], 200) def get_cookie(session: Session, id) -> CookieSchema: cookie = session.query(Cookie).filter_by(id=id).one_or_none() if cookie is None: - logger.warn("Someone keeps requesting bad cookie locations") msg = {"error": "404 Not Found"} - return http.Response(msg, status=404) - return CookieSchema(cookie) + return ExtJSONResponse(msg, 404) + return ExtJSONResponse(CookieSchema(cookie), 200) -def create_cookie(session: Session, json_data: http.RequestData, route: Router): - cookie_data = cookie_schema.load(json_data) +def create_cookie(session: Session, cookie_data: CookieSchema, app: App): cookie = Cookie(**cookie_data) session.add(cookie) session.commit() - headers = {'Location': route.reverse_url('get_cookie', dict(id=cookie.id))} - return http.Response(cookie_schema.dump(cookie), status=201, headers=headers) + headers = {'Location': app.reverse_url('get_cookie', id=cookie.id)} + return ExtJSONResponse(CookieSchema(cookie), 201, headers=headers) def delete_cookie(session: Session, id: int): @@ -63,17 +42,18 @@ def delete_cookie(session: Session, id: int): if cookie is None: msg = {"error": "404 Not Found"} return http.Response(msg, status=404) + logger.debug("Deleting cookie {} {}".format(cookie.id, cookie.name)) session.delete(cookie) - return {"message": "200 OK"} + session.commit() + + return ExtJSONResponse({}, 204) _routes = [ - Route('/state', 'GET', get_state), Route('/cookies', 'GET', get_cookies), Route('/cookies', 'POST', create_cookie), Route('/cookies/{id}', 'GET', get_cookie), - Include('/docs', docs_urls), - Include('/static', static_urls) + Route('/cookies/{id}', 'DELETE', delete_cookie) ] app_settings = { @@ -82,21 +62,24 @@ app_settings = { } } -_routes = _routes + auth_routes +_routes = _routes # + auth_routes -_commands = sqlalchemy_backend.commands + commands + migrate_commands +_hooks = [SQLAlchemyHook] - -_components = sqlalchemy_backend.components + auth_components + [mail_component] +_components = [ + SQLAlchemySession(engine=engine), + JWT({ + 'JWT_SECRET': 'thisisasecret', + }), +] -def application_factory(settings={}, routes=_routes, commands=_commands, components=_components): +def application_factory(settings={}, routes=_routes, components=_components, hooks=_hooks): """Returns an instance of Cookie API""" _settings = {**app_settings, **settings} - logging.global_init(_settings) + global_init(_settings) - return App(settings=_settings, - commands=commands, - components=components, + return App(components=components, + event_hooks=hooks, routes=routes) diff --git a/cookie_api/auth.py b/cookie_api/auth.py index ace17f1..a219ea4 100644 --- a/cookie_api/auth.py +++ b/cookie_api/auth.py @@ -1,20 +1,15 @@ +""" import datetime as dt -from apistar import Component, Settings, http, Route, Include, annotate -from apistar.interfaces import Auth -from apistar.backends.sqlalchemy_backend import Session -from apistar_jwt.authentication import get_jwt, JWTAuthentication +from apistar import Component, http, Route, Include + from apistar_jwt.token import JWT -from apistar_mail import Message, Mail +# from apistar_mail import Message, Mail from sqlalchemy.exc import IntegrityError, InvalidRequestError from sqlalchemy.orm.exc import NoResultFound from cookie_api.models import User -from cookie_api.schema import UserSchema, UserCreateSchema - -auth_components = [ - Component(JWT, init=get_jwt) -] +from cookie_api.schema import def login(settings: Settings, json_data: http.RequestData, session: Session): @@ -110,3 +105,4 @@ routes = [ ] auth_routes = [Include('/auth', routes)] +""" \ No newline at end of file diff --git a/cookie_api/commands.py b/cookie_api/commands.py deleted file mode 100644 index 2341457..0000000 --- a/cookie_api/commands.py +++ /dev/null @@ -1,16 +0,0 @@ -from apistar import Command -from apistar.backends.sqlalchemy_backend import Session - -from cookie_api.models import User - - -def create_user(session: Session, email, password): - user = User(email, password) - session.add(user) - session.commit() - print('User added') - - -commands = [ - Command('create_user', create_user) -] diff --git a/cookie_api/logging.py b/cookie_api/logger.py similarity index 99% rename from cookie_api/logging.py rename to cookie_api/logger.py index 2da5848..302faa4 100644 --- a/cookie_api/logging.py +++ b/cookie_api/logger.py @@ -13,4 +13,3 @@ def global_init(settings={}): else: logbook.StreamHandler(sys.stdout, level=_level).push_application() - diff --git a/cookie_api/renders.py b/cookie_api/renders.py deleted file mode 100644 index 5b6572e..0000000 --- a/cookie_api/renders.py +++ /dev/null @@ -1,25 +0,0 @@ -import datetime as dt -import decimal -import json - -from apistar import http -from apistar.renderers import Renderer - - -def extended_encoder(obj): - """JSON encoder function with support for ISO 8601 datetime serialization and Decimal to float casting""" - if isinstance(obj, dt.datetime): - return obj.isoformat() - elif isinstance(obj, decimal.Decimal): - return float(obj) - - -class JSONRenderer(Renderer): - """JSON Render with support for ISO 8601 datetime serialization and Decimal to float casting""" - media_type = 'application/json' - charset = None - - def render(self, data: http.ResponseData) -> bytes: - return json.dumps(data, default=extended_encoder).encode('utf-8') - -# TODO add an XML render diff --git a/cookie_api/schema.py b/cookie_api/schema.py index 49cd572..ee93d8e 100644 --- a/cookie_api/schema.py +++ b/cookie_api/schema.py @@ -1,48 +1,13 @@ -from apistar import typesystem -# from marshmallow import Schema, fields - -from cookie_api.types import Datetime, Decimal - - -# class CookieSchema(Schema): -# id = fields.Int() -# created_date = fields.DateTime() -# modified_date = fields.DateTime() -# name = fields.Str(required=True) -# recipe_url = fields.Str() -# sku = fields.Str(required=True) -# qoh = fields.Int(required=True) -# unit_cost = fields.Decimal(required=True) - - -class UserSchema(typesystem.Object): - description = 'A User respresentation' - properties = { - 'id': typesystem.integer(), - 'created_date': Datetime, - 'modified_date': Datetime, - 'email': typesystem.string(max_length=255), - 'confirmed': typesystem.boolean(), - 'admin': typesystem.boolean() - } - - -class UserCreateSchema(typesystem.Object): - description = 'A User respresentation for creating a user' - properties = { - 'email': typesystem.string(max_length=255), - 'password': typesystem.string(max_length=255) - } - - -class CookieSchema(typesystem.Object): - properties = { - 'id': typesystem.integer(), - 'created_date': Datetime, - 'modified_date': Datetime, - 'name': typesystem.string(), - 'recipe_url': typesystem.string(), - 'sku': typesystem.string(), - 'qoh': typesystem.integer(), - 'unit_cost': Decimal - } \ No newline at end of file +from apistar import types, validators +from cookie_api.util import Decimal + + +class CookieSchema(types.Type): + id = validators.Integer(allow_null=True) + created_date = validators.DateTime(allow_null=True) + modified_date = validators.DateTime(allow_null=True) + name = validators.String(max_length=50) + recipe_url = validators.String(max_length=255) + sku = validators.String(max_length=55) + qoh = validators.Integer(minimum=0) + unit_cost = Decimal(minimum=0.0) diff --git a/cookie_api/types.py b/cookie_api/types.py deleted file mode 100644 index 9d5b13a..0000000 --- a/cookie_api/types.py +++ /dev/null @@ -1,25 +0,0 @@ -import datetime -import decimal - -import dateutil -from apistar import typesystem - - -class Datetime(datetime.datetime): - native_type = datetime.datetime - - def __new__(cls, *args, **kwargs) -> datetime: - if args and isinstance(args[0], cls.native_type): - return args[0] - if args and isinstance(args[0], str): - try: - return dateutil.parser.parse(args[0]) - except ValueError: - raise typesystem.TypeSystemError(cls=cls, code='type') from None - return cls.native_type(*args, **kwargs) - - -class Decimal(typesystem._NumericType, decimal.Decimal): - native_type = decimal.Decimal - - diff --git a/cookie_api/util.py b/cookie_api/util.py new file mode 100644 index 0000000..6229197 --- /dev/null +++ b/cookie_api/util.py @@ -0,0 +1,106 @@ +import datetime as dt +import decimal +from math import isfinite +import typing +from apistar import types, validators +from apistar.http import JSONResponse, Response +from apistar.server.components import Component +from sqlalchemy.engine import Engine +from sqlalchemy.orm import sessionmaker, Session, scoped_session + + +class Decimal(validators.NumericType): + numeric_type = decimal.Decimal + + def validate(self, value, definitions=None, allow_coerce=False): + if value is None and self.allow_null: + return None + elif value is None: + self.error('null') + elif isinstance(value, bool): + self.error('type') + elif self.numeric_type is int and isinstance(value, float) and not value.is_integer(): + self.error('integer') + elif not isinstance(value, (int, float, decimal.Decimal)) and not allow_coerce: + self.error('type') + elif isinstance(value, float) and not isfinite(value): + self.error('finite') + + try: + value = self.numeric_type(value) + except (TypeError, ValueError): + self.error('type') + + if self.enum is not None: + if value not in self.enum: + if len(self.enum) == 1: + self.error('exact') + self.error('enum') + + if self.minimum is not None: + if self.exclusive_minimum: + if value <= self.minimum: + self.error('exclusive_minimum') + else: + if value < self.minimum: + self.error('minimum') + + if self.maximum is not None: + if self.exclusive_maximum: + if value >= self.maximum: + self.error('exclusive_maximum') + else: + if value > self.maximum: + self.error('maximum') + + if self.multiple_of is not None: + if isinstance(self.multiple_of, float): + if not (value * (1 / self.multiple_of)).is_integer(): + self.error('multiple_of') + else: + if value % self.multiple_of: + self.error('multiple_of') + + return value + + +class ExtJSONResponse(JSONResponse): + """JSON Response with support for ISO 8601 datetime serialization and Decimal to float casting""" + + def default(self, obj: typing.Any) -> typing.Any: + if isinstance(obj, types.Type): + return dict(obj) + if isinstance(obj, dt.datetime): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return float(obj) + error = "Object of type '%s' is not JSON serializable." + return TypeError(error % type(obj).__name_) + + +DBSession = scoped_session(sessionmaker()) + + +class SQLAlchemySession(Component): + def __init__(self, engine=None): + if not isinstance(engine, Engine): + raise ValueError('SQLAlchemySession must be instantiated with a sqlalchemy.engine.Engine object') + self.engine = engine + DBSession.configure(bind=self.engine) + + def resolve(self) -> Session: + return DBSession() + + +class SQLAlchemyHook: + def on_request(self, session: Session): + return + + def on_response(self, session: Session, response: Response): + DBSession.remove() + return response + + def on_error(self, session: Session, response: Response): + session.rollback() + DBSession.remove() + return response \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7486ced..d975550 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,66 +1,23 @@ -alembic==0.9.6 -apistar==0.3.9 -apistar-alembic-migrations==0.0.6 -apistar-jwt==0.2.1 +alembic==0.9.9 +apistar==0.5.10 +apistar-jwt==0.4.2 bcrypt==3.1.4 -certifi==2017.7.27.1 -cffi==1.11.2 +certifi==2018.4.16 +cffi==1.11.5 chardet==3.0.4 -colorama==0.3.9 -coreapi==2.3.3 -coreschema==0.0.4 idna==2.6 -itypes==1.1.0 -Jinja2==2.9.6 -Logbook==1.1.0 +Jinja2==2.10 +Logbook==1.3.3 Mako==1.0.7 MarkupSafe==1.0 -marshmallow==2.15.0 -psycopg2==2.7.3.1 -py==1.4.34 +psycopg2==2.7.4 pycparser==2.18 -PyJWT==1.5.3 -pytest==3.2.3 -python-dateutil==2.6.1 +PyJWT==1.6.1 +python-dateutil==2.7.2 python-editor==1.0.3 requests==2.18.4 six==1.11.0 -SQLAlchemy==1.1.14 -uritemplate==3.0.0 +SQLAlchemy==1.2.7 urllib3==1.22 -Werkzeug==0.12.2 -whitenoise==3.3.1 -alembic==0.9.6 -apistar==0.3.9 -apistar-alembic-migrations==0.0.6 -apistar-jwt==0.2.1 -apistar-mail==0.2.0 -bcrypt==3.1.4 -certifi==2017.7.27.1 -cffi==1.11.2 -chardet==3.0.4 -colorama==0.3.9 -coreapi==2.3.3 -coreschema==0.0.4 -idna==2.6 -itsdangerous==0.24 -itypes==1.1.0 -Jinja2==2.9.6 -Logbook==1.1.0 -Mako==1.0.7 -MarkupSafe==1.0 -marshmallow==2.15.0 -psycopg2==2.7.3.1 -py==1.4.34 -pycparser==2.18 -PyJWT==1.5.3 -pytest==3.2.3 -python-dateutil==2.6.1 -python-editor==1.0.3 -requests==2.18.4 -six==1.11.0 -SQLAlchemy==1.1.14 -uritemplate==3.0.0 -urllib3==1.22 -Werkzeug==0.12.2 +Werkzeug==0.14.1 whitenoise==3.3.1 diff --git a/wsgi.py b/wsgi.py index d4f1906..b62adfd 100644 --- a/wsgi.py +++ b/wsgi.py @@ -3,12 +3,13 @@ try: except ImportError: # Fall back to psycopg2cffi from psycopg2cffi import compat + compat.register() from cookie_api import application_factory -from config import settings -app = application_factory(settings) +app = application_factory() -if __name__ == "__main__": - app.main() +if __name__ == '__main__': + app = application_factory() + app.serve('127.0.0.1', 5000, debug=True)