Compare commits

..

20 Commits

Author SHA1 Message Date
androiddrew c7d65e06d2 Added active attribute to the users table 7 years ago
androiddrew 3ef86bca1e Fixed migrations and dev requirements 7 years ago
androiddrew e67b099e89 Adding pip-tools and pinning requirements 7 years ago
androiddrew ad25ca2bf9 added itsdangerous dependency 7 years ago
androiddrew fd6b3a36ce fixed manifest recursive includes 7 years ago
androiddrew f55b21a6d6 Modifying setup for .deb package creation and fixing requirements 7 years ago
androiddrew de9ff6adc9 Added responsive email and fixed dwsgi runner 7 years ago
androiddrew 12bc49100b Added registration email confirmation. Refactored confirm to wsgy runner 7 years ago
androiddrew 442b8acdc5 Updating dependencies and tests 7 years ago
androiddrew 101698c348 Bumper APIStar version to 0.5.12 7 years ago
androiddrew a2e3ad84c1 Added an index page the hits the API for some data 7 years ago
androiddrew b048fe1254 Refactored resources out to own submodule 7 years ago
androiddrew bd818f1cf2 Upgraded to Timezone Aware Datetimes 7 years ago
androiddrew 4f4099d998 Leveled up a lot of code. Should have committed more frequently 7 years ago
androiddrew e3e4ba7ace added back user registration endpoint 7 years ago
androiddrew 922f8c7d52 Added JWT back into Project 7 years ago
androiddrew d0a9b556c6 leveled up, minus auth 7 years ago
androiddrew 7b23ad97d8 Moving to Typesystem
Moving to type system. Added registration endpoint. Added custom Decimal type.
7 years ago
androiddrew 3ef15e7b48 added user profile endpoint 7 years ago
androiddrew a52d431e0d Included support for apistar-mail 7 years ago

6
.gitignore vendored

@ -51,6 +51,7 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*,cover *,cover
.pytest_cache/
# Translations # Translations
*.mo *.mo
@ -66,4 +67,7 @@ docs/_build/
/dump.rdb /dump.rdb
# Project # Project
config.py config.py
# Mac
.DS_Store

@ -0,0 +1,2 @@
recursive-include cookie_api/templates *
recursive-include cookie_api/static *

@ -0,0 +1,33 @@
# Cookie API
The Cookie API serves as a simple experimentation application for testing concepts in the APIStar platform. It is intended to help drive the design of the APIStar framework, uncover new design patterns, and apply best practices in application development.
## Features include:
* CRUD Interface for a simple domain problem
* JWT Authentication
* Custom JSON responses
* Automated Testing
* Logging
## Features not yet implemented:
* Template rendering for a Vuejs SPA client (not yet implemented)
* Email Confirmation
* Password Reset
* Authorization model for user
* Rate Limiting
* Automated deployment
* JSON API Responses
* Multipart file uploads
* Server Side Rendering of Vuejs Client
* Async Task Workers
* Error monitoring through Sentry
# Testing
```python
pip install -e .[testing]
pytest
```

@ -35,7 +35,7 @@ script_location = migrations
# are written from script.py.mako # are written from script.py.mako
# output_encoding = utf-8 # output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname sqlalchemy.url = postgresql://apistar@localhost:5432/apistar
# Logging configuration # Logging configuration

@ -1,3 +1 @@
from cookie_api.renders import JSONRenderer from cookie_api.app import application_factory
from cookie_api.app import application_factory
from cookie_api.models import Base

@ -1,75 +1,23 @@
from apistar import Include, Route, http, annotate from apistar import Route
from apistar.backends import sqlalchemy_backend from apistar.http import HTMLResponse
from apistar.backends.sqlalchemy_backend import Session from apistar_jwt import JWT
from apistar.frameworks.wsgi import WSGIApp as App from apistar_mail import MailComponent
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
import logbook import logbook
from sqlalchemy import create_engine
from pathlib import Path
from cookie_api.auth import auth_routes
from cookie_api.logger import global_init
from cookie_api.resources import cookie_routes
from cookie_api.util import SQLAlchemyHook, SQLAlchemySession, MetaApp as App
from cookie_api.auth import auth_routes, auth_components BASEDIR = Path(__file__).parent
from cookie_api.commands import commands TEMPLATE_DIR = BASEDIR.joinpath('templates')
from cookie_api.models import Cookie STATIC_DIR = BASEDIR.joinpath('static')
from cookie_api.schema import CookieSchema
from cookie_api import logging
cookie_schema = CookieSchema() logger = logbook.Logger(__name__)
logger = logbook.Logger('Cookies') engine = create_engine('postgresql://apistar@localhost:5432/apistar')
@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):
logger.info("Accessing the Cookies resource")
cookies = session.query(Cookie).all()
return cookie_schema.dump(cookies, many=True).data
def get_cookie(session: Session, id):
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 cookie_schema.dump(cookie).data
def create_cookie(session: Session, json_data: http.RequestData, route: Router):
cookie_data = cookie_schema.load(json_data)
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)
def delete_cookie(session: Session, id: int):
cookie = session.query(Cookie).filter_by(id=id).one_or_none()
if cookie is None:
msg = {"error": "404 Not Found"}
return http.Response(msg, status=404)
session.delete(cookie)
return {"message": "200 OK"}
_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)
]
app_settings = { app_settings = {
"LOGGING": { "LOGGING": {
@ -77,20 +25,47 @@ app_settings = {
} }
} }
_routes = _routes + auth_routes
_commands = sqlalchemy_backend.commands + commands + migrate_commands def return_index_html():
with open(STATIC_DIR.joinpath('index.html'), mode='rb') as f:
return HTMLResponse(content=f.read())
_components = sqlalchemy_backend.components + auth_components index = Route('/', 'GET', return_index_html)
_routes = cookie_routes + auth_routes + [index]
_hooks = [SQLAlchemyHook]
_components = [
SQLAlchemySession(engine=engine),
JWT({
'JWT_USER_ID': 'sub',
'JWT_SECRET': 'thisisasecret',
}),
MailComponent(**{
'MAIL_SERVER': 'mail.example.com',
'MAIL_USERNAME': 'me@example.com',
'MAIL_PASSWORD': 'donotsavethistoversioncontrol',
'MAIL_PORT': 587,
'MAIL_USE_TLS': True,
'MAIL_DEFAULT_SENDER': 'me@example.com',
'MAIL_SUPPRESS_SEND': True
})
]
def application_factory(settings={}, routes=_routes, commands=_commands, components=_components): def application_factory(routes=_routes, components=_components, hooks=_hooks, settings={}, template_dir=TEMPLATE_DIR,
static_dir=STATIC_DIR):
"""Returns an instance of Cookie API""" """Returns an instance of Cookie API"""
_settings = {**app_settings, **settings} _settings = {**app_settings, **settings}
logging.global_init(_settings) global_init(_settings)
logger.debug("Template directory {}".format(template_dir))
logger.debug("Static directory {}".format(STATIC_DIR))
return App(settings=_settings, return App(components=components,
commands=commands, event_hooks=hooks,
components=components, routes=routes,
routes=routes) template_dir=str(template_dir), # have to use name because of Jinja2
static_dir=static_dir)

@ -1,36 +1,72 @@
import datetime as dt import datetime as dt
import re
from apistar import Component, Settings, http, Route, Include from apistar.exceptions import HTTPException
from apistar.backends.sqlalchemy_backend import Session
from apistar_jwt.authentication import get_jwt from apistar import http, Route, Include, App
from apistar_jwt.token import JWT from apistar_jwt.token import JWT, JWTUser
from sqlalchemy.exc import IntegrityError, InvalidRequestError from apistar_mail import Message, Mail
# from sqlalchemy.exc import IntegrityError, InvalidRequestError
from itsdangerous import URLSafeTimedSerializer, BadSignature
import logbook
from sqlalchemy.orm import Session
from sqlalchemy.orm.exc import NoResultFound
from cookie_api.models import User from cookie_api.models import User
from cookie_api.schema import UserExportSchema, UserCreateSchema
from cookie_api.util import ExtJSONResponse
# https://realpython.com/handling-email-confirmation-in-flask/
EMAIL_REGEX = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)"
email_regex = re.compile(EMAIL_REGEX)
logger = logbook.Logger(__name__)
def generate_confirmation_token(email):
serializer = URLSafeTimedSerializer('secret-key')
return serializer.dumps(email, salt='password-salt')
auth_components = [
Component(JWT, init=get_jwt)
]
def confirm_token(token, expiration=3600):
serializer = URLSafeTimedSerializer('secret-key')
try:
email = serializer.loads(
token,
salt='password-salt',
max_age=expiration
)
except BadSignature:
return False
return email
def login(settings: Settings, json_data: http.RequestData, session: Session):
def login(json_data: http.RequestData, session: Session, jwt: JWT):
user_id = json_data.get('email') user_id = json_data.get('email')
password = json_data.get('password') password = json_data.get('password')
user = session.query(User).filter_by(email=user_id).one() try:
user = session.query(User).filter_by(email=user_id).one()
except NoResultFound:
error = {
'status': 'fail',
'message': 'User does not exist'
}
return ExtJSONResponse(error, status=400, headers={'WWW-Authenticate': 'Bearer'})
if not user.check_password(password): if not user.check_password(password):
error = {'error': 'Password auth failed'}, error = {'error': 'Password auth failed'},
return http.Response(error, status=401, headers={'WWW-Authenticate': 'Bearer'}) return ExtJSONResponse(error, status=401, headers={'WWW-Authenticate': 'Bearer'})
secret = settings['JWT'].get('SECRET')
payload = { payload = {
'exp': dt.datetime.utcnow() + dt.timedelta(days=0, minutes=60), 'exp': dt.datetime.utcnow() + dt.timedelta(days=0, minutes=60), # Expiration date of the token
'iat': dt.datetime.utcnow(), 'iat': dt.datetime.utcnow(), # the time the token was generated
'sub': user.id 'sub': user.id # the subject of the token
} }
token = JWT.encode(payload, secret=secret) token = jwt.encode(payload)
data = { data = {
'status': 'success', 'status': 'success',
@ -38,7 +74,7 @@ def login(settings: Settings, json_data: http.RequestData, session: Session):
'auth_token': token 'auth_token': token
} }
return data return ExtJSONResponse(data, 200)
# TODO Add user logout # TODO Add user logout
@ -46,42 +82,71 @@ def logout():
pass pass
# TODO Add user registration def register(user_data: UserCreateSchema, session: Session, mail: Mail, app: App) -> UserExportSchema:
def register(settings: Settings, json_data: http.RequestData, session: Session): if not email_regex.match(user_data['email']):
user_id = json_data.get('email') raise HTTPException('Please provide a valid email address', status_code=400)
password = json_data.get('password')
email_check = session.query(User).filter_by(email=user_id).one_or_none() email_check = session.query(User).filter_by(email=user_data['email']).one_or_none()
if email_check is not None: if email_check is not None:
message = { raise HTTPException('user email address is already in use', status_code=400)
'status': 'error',
'message': 'user email address is already in use' user = User(email=user_data['email'], password=user_data['password'])
}
return http.Response(message, status=400) try:
tokenized_email = generate_confirmation_token(user.email)
email_body = app.render_template('confirm_email.jinja2',
confirm_url=f'http://localhost:5000/auth/confirm?token={tokenized_email}',
logo_url='http://localhost:5000/static/cookie_icon.png'
)
user = User(email=user_id, password=password) msg = Message(subject="Confirm your email",
html=email_body,
recipients=[user_data['email']])
mail.send(msg)
except Exception as e:
logger.error(e, exc_info=True)
raise HTTPException('Error an sending email', status_code=500)
session.add(user) session.add(user)
session.commit() session.commit()
# TODO Send off an email confirmation
headers = {} headers = {}
message = { message = {
'status': 'success', 'message': 'Please check your inbox and confirm your email',
'message': 'Please check your inbox and confirm your email' 'data': UserExportSchema(user)
} }
return http.Response(message, status=200, headers=headers) return ExtJSONResponse(message, 201, headers)
# TODO Add user profile endpoint # TODO Return an HTML response since the user will be clicking on a link in a web browser
def user_profile(): # TODO Since you are returning a user respresentation add a location and Content-Location header
pass def confirm(params: http.QueryParams, session: Session):
token = params['token']
email = confirm_token(token)
if not email:
raise HTTPException('Email token is expired or invalid', status_code=400)
# TODO Add email confirmation user = session.query(User).filter_by(email=email).one()
def confirm(settings: Settings, json_data: http.RequestData, session: Session):
pass if user.confirmed:
raise HTTPException('Account already confirmed. Please login.', status_code=400)
user.confirmed = True
session.add(user)
session.commit()
return ExtJSONResponse(UserExportSchema(user))
def user_profile(user: JWTUser, session: Session) -> UserExportSchema:
try:
user = session.query(User).filter_by(id=user.id).one()
except NoResultFound as e:
error = {'message': str(e)}
raise HTTPException(error, status_code=400)
return ExtJSONResponse(UserExportSchema(user))
# TODO Add email password reset # TODO Add email password reset
@ -91,7 +156,9 @@ def reset():
routes = [ routes = [
Route('/login', 'POST', login), Route('/login', 'POST', login),
Route('/register', 'POST', register) Route('/register', 'POST', register),
Route('/status', 'GET', user_profile),
Route('/confirm', 'GET', confirm)
] ]
auth_routes = [Include('/auth', routes)] auth_routes = [Include('/auth', name='dirp', routes=routes)]

@ -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)
]

@ -13,4 +13,3 @@ def global_init(settings={}):
else: else:
logbook.StreamHandler(sys.stdout, level=_level).push_application() logbook.StreamHandler(sys.stdout, level=_level).push_application()

@ -2,6 +2,7 @@ import bcrypt
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean, Numeric from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Boolean, Numeric
from sqlalchemy.orm import relationship, backref from sqlalchemy.orm import relationship, backref
from sqlalchemy.schema import CreateColumn
from sqlalchemy.sql import expression from sqlalchemy.sql import expression
from sqlalchemy.ext.compiler import compiles from sqlalchemy.ext.compiler import compiles
from sqlalchemy.types import DateTime as DateTimeType from sqlalchemy.types import DateTime as DateTimeType
@ -20,13 +21,20 @@ def pg_utcnow(element, compiler, **kw):
return "TIMEZONE('utc', CURRENT_TIMESTAMP)" 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
Base = declarative_base() Base = declarative_base()
class DBMixin: class DBMixin:
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
created_date = Column(DateTime, server_default=utcnow()) created_date = Column(DateTime(timezone=True), server_default=utcnow())
modified_date = Column(DateTime, server_default=utcnow(), onupdate=utcnow()) modified_date = Column(DateTime(timezone=True), server_default=utcnow(), onupdate=utcnow())
def to_dict(self): def to_dict(self):
d = self.__dict__.copy() d = self.__dict__.copy()
@ -56,6 +64,7 @@ class User(DBMixin, Base):
password = Column(String(255)) password = Column(String(255))
admin = Column(Boolean, nullable=False, default=False) admin = Column(Boolean, nullable=False, default=False)
confirmed = Column(Boolean, nullable=False, default=False) confirmed = Column(Boolean, nullable=False, default=False)
active = Column(Boolean, nullable=False, default=True)
def __init__(self, email, password, admin=False): def __init__(self, email, password, admin=False):
self.email = email self.email = email
@ -65,6 +74,13 @@ class User(DBMixin, Base):
def check_password(self, password): def check_password(self, password):
return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8')) return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8'))
def to_dict(self):
d = self.__dict__.copy()
if '_sa_instance_state' in d:
d.pop('_sa_instance_state')
d.pop('password')
return d
class Order(DBMixin, Base): class Order(DBMixin, Base):
__tablename__ = 'orders' __tablename__ = 'orders'

@ -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

@ -0,0 +1 @@
from .cookies import cookie_routes

@ -0,0 +1,57 @@
import typing
import logbook
from apistar import http, Route, App
from apistar_jwt import authentication_required, JWTUser
from sqlalchemy.orm import Session
from cookie_api.models import Cookie
from cookie_api.schema import CookieSchema
from cookie_api.util import MetaJSONResponse
logger = logbook.Logger(__name__)
def get_cookies(session: Session) -> typing.List[CookieSchema]:
cookies = session.query(Cookie).all()
logger.debug('Cookies collection hit')
return MetaJSONResponse([CookieSchema(cookie) for cookie in cookies])
def get_cookie(session: Session, id) -> CookieSchema:
cookie = session.query(Cookie).filter_by(id=id).one_or_none()
if cookie is None:
msg = {"error": "404 Not Found"}
return MetaJSONResponse(msg, 404)
return MetaJSONResponse(CookieSchema(cookie))
@authentication_required
def create_cookie(session: Session, cookie_data: CookieSchema, app: App, user: JWTUser):
cookie = Cookie(**cookie_data)
session.add(cookie)
session.commit()
headers = {'Location': app.reverse_url('get_cookie', id=cookie.id)}
return MetaJSONResponse(CookieSchema(cookie), 201, headers)
@authentication_required
def delete_cookie(session: Session, id: int, user: JWTUser):
cookie = session.query(Cookie).filter_by(id=id).one_or_none()
if cookie is None:
msg = {"error": "404 Not Found"}
return MetaJSONResponse(msg, 204)
logger.debug("Deleting cookie {} {}".format(cookie.id, cookie.name))
session.delete(cookie)
session.commit()
return MetaJSONResponse({}, 204)
cookie_routes = [
Route('/cookies', 'GET', get_cookies),
Route('/cookies', 'POST', create_cookie),
Route('/cookies/{id}', 'GET', get_cookie),
Route('/cookies/{id}', 'DELETE', delete_cookie)
]

@ -1,12 +1,27 @@
from marshmallow import Schema, fields from apistar import types, validators
from cookie_api.util import Decimal
class CookieSchema(Schema):
id = fields.Int() class CookieSchema(types.Type):
created_date = fields.DateTime() id = validators.Integer(allow_null=True)
modified_date = fields.DateTime() created_date = validators.DateTime(allow_null=True)
name = fields.Str(required=True) modified_date = validators.DateTime(allow_null=True)
recipe_url = fields.Str() name = validators.String(max_length=50)
sku = fields.Str(required=True) recipe_url = validators.String(max_length=255)
qoh = fields.Int(required=True) sku = validators.String(max_length=55)
unit_cost = fields.Decimal(required=True) qoh = validators.Integer(minimum=0)
unit_cost = Decimal(minimum=0.0)
class UserCreateSchema(types.Type):
email = validators.String(max_length=255)
password = validators.String(max_length=255)
class UserExportSchema(types.Type):
id = validators.Integer()
created_date = validators.DateTime(allow_null=True)
modified_date = validators.DateTime(allow_null=True)
email = validators.String()
admin = validators.Boolean()
confirmed = validators.Boolean()

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Cookie API</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<h1>APIStar Cookies</h1>
<div id="app">
<ul>
<li v-for="cookie in cookies">
{{ cookie.name }}
</li>
</ul>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
cookies: []
},
created() {
axios.get("http://localhost:5000/cookies").then(
(response) => {
this.cookies = response.data.data
})
},
});
</script>
</body>
</html>

@ -0,0 +1,310 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
.ReadMsgBody {
width: 100%;
}
.ExternalClass {
width: 100%;
}
.ExternalClass * {
line-height: 100%;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if !mso]><!-->
<style type="text/css">
@media only screen and (max-width:480px) {
@-ms-viewport {
width: 320px;
}
@viewport {
width: 320px;
}
}
</style>
<!--<![endif]-->
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
}
}
</style>
<style type="text/css">
</style>
</head>
<body>
<div>
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="Margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:100px;">
<img height="auto" src="{{ logog_url }}" style="border:0;display:block;outline:none;text-decoration:none;width:100%;" width="100" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:helvetica;font-size:32px;line-height:1;text-align:center;color:#01B48F;">
Cookie API
</div>
</td>
</tr>
<tr>
<td style="font-size:0px;padding:10px 25px;word-break:break-word;">
<p style="border-top:solid 4px #01B48F;font-size:1;margin:0px auto;width:100%;">
</p>
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" style="border-top:solid 4px #01B48F;font-size:1;margin:0px auto;width:550px;" role="presentation" width="550px"
>
<tr>
<td style="height:0;line-height:0;">
&nbsp;
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="Margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;vertical-align:top;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 outlook-group-fix" style="font-size:13px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:helvetica;font-size:16px;line-height:1;text-align:center;color:#000000;">
Welcome to the Cookie API.
</div>
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:helvetica;font-size:16px;line-height:1;text-align:center;color:#000000;">
Please click the button below to confirm your email account.
</div>
</td>
</tr>
<tr>
<td align="center" vertical-align="middle" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:separate;line-height:100%;">
<tr>
<td align="center" bgcolor="#FF9A57" role="presentation" style="border:none;border-radius:3px;color:#ffffff;cursor:auto;padding:10px 25px;" valign="middle">
<a href="{{ confirm_url }}" style="background:#FF9A57;color:#ffffff;font-family:Ubuntu, Helvetica, Arial, sans-serif;font-size:13px;font-weight:normal;line-height:120%;Margin:0;text-decoration:none;text-transform:uppercase;" target="_blank">
Confirm Email
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</div>
</body>
</html>

@ -1,19 +0,0 @@
import datetime
import dateutil
from apistar.typesystem import TypeSystemError
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 TypeSystemError(cls=cls, code='type') from None
return cls.native_type(*args, **kwargs)

@ -0,0 +1,5 @@
from .app import MetaApp
from .http import MetaJSONResponse, ExtJSONResponse
from .component import SQLAlchemySession, DBSession
from .hook import SQLAlchemyHook
from .validators import Decimal

@ -0,0 +1,28 @@
import sys
from apistar import App, exceptions
from apistar.http import Response, HTMLResponse
from apistar.server.components import ReturnValue
from .http import MetaJSONResponse
class MetaApp(App):
"""
A WSGI App subclass with a MetaJSONResponse default response type
"""
def render_response(self, return_value: ReturnValue) -> Response:
if isinstance(return_value, Response):
return return_value
elif isinstance(return_value, str):
return HTMLResponse(return_value)
return MetaJSONResponse(return_value)
def exception_handler(self, exc: Exception) -> Response:
if isinstance(exc, exceptions.HTTPException):
return MetaJSONResponse(exc.detail, status_code=exc.status_code, headers=exc.get_headers())
raise
def error_handler(self) -> Response:
return MetaJSONResponse('Server error', status_code=500, exc_info=sys.exc_info())

@ -0,0 +1,17 @@
from apistar import Component
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
from sqlalchemy.orm import sessionmaker, Session, scoped_session
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()

@ -0,0 +1,15 @@
from apistar.http import Response
from .component import Session, 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

@ -0,0 +1,85 @@
import datetime as dt
import decimal
import json
import typing
from apistar import types, exceptions
from apistar.http import JSONResponse, Response, StrMapping, StrPairs, MutableHeaders
from werkzeug.http import HTTP_STATUS_CODES
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_)
class MetaJSONResponse(Response):
"""A JSONResponse that returns a meta, data, error on response"""
media_type = 'application/json'
charset = None
options = {
'ensure_ascii': False,
'allow_nan': False,
'indent': None,
'separators': (',', ':'),
}
def __init__(self,
content: typing.Any,
status_code: int = 200,
headers: typing.Union[StrMapping, StrPairs] = None,
exc_info=None,
meta: typing.Any = None) -> None:
self.status_code = status_code
self.meta = self._build_meta(meta)
self._content = content
self.content = self.render(content)
self.headers = MutableHeaders(headers)
self.set_default_headers()
self.exc_info = exc_info
def render(self, content: typing.Any) -> bytes:
"""Builds a JSON response containing meta data. If the content is an `apistar.execption.HTTPException`
then it will return the execption reason code"""
error = {}
if isinstance(content, exceptions.HTTPException):
error["reason"] = content.detail
options = {'default': self.default}
options.update(self.options)
response = dict(meta=self.meta)
if error:
response.update(dict(error=error))
return json.dumps(response, **options).encode('utf-8')
response.update(dict(data=content))
return json.dumps(response, **options).encode('utf-8')
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_)
def _build_meta(self, meta):
_meta = {
"status": self.status_code,
"message": HTTP_STATUS_CODES.get(self.status_code)
}
if meta is None:
return _meta
return {**_meta, **meta}

@ -0,0 +1,59 @@
import decimal
from math import isfinite
from apistar import validators
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

@ -0,0 +1,4 @@
alembic
pytest
pytest-cov
tox

@ -0,0 +1,23 @@
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file dev_requirements.txt dev_requirements.in
#
alembic==0.9.9
atomicwrites==1.1.5 # via pytest
attrs==18.1.0 # via pytest
coverage==4.5.1 # via pytest-cov
mako==1.0.7 # via alembic
markupsafe==1.0 # via mako
more-itertools==4.2.0 # via pytest
pluggy==0.6.0 # via pytest, tox
py==1.5.3 # via pytest, tox
pytest-cov==2.5.1
pytest==3.6.1
python-dateutil==2.7.3 # via alembic
python-editor==1.0.3 # via alembic
six==1.11.0 # via more-itertools, pytest, python-dateutil, tox
sqlalchemy==1.2.8 # via alembic
tox==3.0.0
virtualenv==16.0.0 # via tox

@ -2,7 +2,7 @@ from __future__ import with_statement
from alembic import context from alembic import context
from sqlalchemy import engine_from_config, pool from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig from logging.config import fileConfig
from cookie_api import Base from cookie_api.models import Base
# this is the Alembic Config object, which provides # this is the Alembic Config object, which provides
# access to the values within the .ini file in use. # access to the values within the .ini file in use.

@ -1,28 +0,0 @@
"""initial models
Revision ID: 227892845cde
Revises:
Create Date: 2018-01-15 14:58:17.931063
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '227892845cde'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
pass
# ### end Alembic commands ###

@ -0,0 +1,75 @@
"""Cookie Domain Model
Revision ID: 374c36260db7
Revises: 710505cf5d4c
Create Date: 2018-06-10 17:39:18.891568
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '374c36260db7'
down_revision = '710505cf5d4c'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('cookies',
sa.Column('id', sa.Integer(), 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('name', sa.String(length=50), nullable=True),
sa.Column('recipe_url', sa.String(length=255), nullable=True),
sa.Column('sku', sa.String(length=55), nullable=True),
sa.Column('qoh', sa.Integer(), nullable=True),
sa.Column('unit_cost', sa.Numeric(precision=12, scale=2), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_cookies_name'), 'cookies', ['name'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), 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('email', sa.String(length=255), nullable=False),
sa.Column('password', sa.String(length=255), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=False),
sa.Column('confirmed', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email')
)
op.create_table('orders',
sa.Column('id', sa.Integer(), 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('user_id', sa.Integer(), nullable=False),
sa.Column('shipped', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('line_items',
sa.Column('id', sa.Integer(), 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('order_id', sa.Integer(), nullable=False),
sa.Column('cookie_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=True),
sa.Column('extended_cost', sa.Numeric(precision=12, scale=2), nullable=True),
sa.ForeignKeyConstraint(['cookie_id'], ['cookies.id'], ),
sa.ForeignKeyConstraint(['order_id'], ['orders.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('line_items')
op.drop_table('orders')
op.drop_table('users')
op.drop_index(op.f('ix_cookies_name'), table_name='cookies')
op.drop_table('cookies')
# ### end Alembic commands ###

@ -0,0 +1,24 @@
"""Empty Init
Revision ID: 710505cf5d4c
Revises:
Create Date: 2018-06-10 17:31:37.014032
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '710505cf5d4c'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass

@ -0,0 +1,30 @@
"""Adding user active attribute
Revision ID: 7c2d43ec9c84
Revises: 374c36260db7
Create Date: 2018-06-10 17:57:32.570036
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7c2d43ec9c84'
down_revision = '374c36260db7'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('active', sa.Boolean()))
op.execute('UPDATE users SET active = True')
op.alter_column('users', 'active', nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'active')
# ### end Alembic commands ###

@ -1,30 +0,0 @@
"""Added confirmed to user
Revision ID: 82595a1e5193
Revises: 227892845cde
Create Date: 2018-01-15 15:34:46.028181
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '82595a1e5193'
down_revision = '227892845cde'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('confirmed', sa.Boolean()))
op.execute('UPDATE users SET confirmed=FALSE')
op.alter_column('users', 'confirmed', nullable=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'confirmed')
# ### end Alembic commands ###

@ -1,32 +1,29 @@
alembic==0.9.6 #
apistar==0.3.9 # This file is autogenerated by pip-compile
apistar-alembic-migrations==0.0.6 # To update, run:
apistar-jwt==0.2.1 #
# pip-compile --output-file requirements.txt setup.py
#
apistar-jwt==0.4.2
apistar-mail==0.3.0
apistar==0.5.18
bcrypt==3.1.4 bcrypt==3.1.4
certifi==2017.7.27.1 certifi==2018.4.16 # via requests
cffi==1.11.2 cffi==1.11.5 # via bcrypt
chardet==3.0.4 chardet==3.0.4 # via requests
colorama==0.3.9 click==6.7 # via apistar
coreapi==2.3.3 idna==2.6 # via requests
coreschema==0.0.4 itsdangerous==0.24
idna==2.6 jinja2==2.10 # via apistar
itypes==1.1.0 logbook==1.3.3
Jinja2==2.9.6 markupsafe==1.0 # via jinja2
Logbook==1.1.0 psycopg2-binary==2.7.4
Mako==1.0.7 pycparser==2.18 # via cffi
MarkupSafe==1.0 pyjwt==1.6.4 # via apistar-jwt
marshmallow==2.15.0 pyyaml==3.12 # via apistar
psycopg2==2.7.3.1 requests==2.18.4 # via apistar
py==1.4.34 six==1.11.0 # via bcrypt
pycparser==2.18 sqlalchemy==1.2.8
PyJWT==1.5.3 urllib3==1.22 # via requests
pytest==3.2.3 werkzeug==0.14.1 # via apistar
python-dateutil==2.6.1 whitenoise==3.3.1 # via apistar
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
whitenoise==3.3.1

@ -0,0 +1,56 @@
from setuptools import setup, find_packages
with open('README.md') as readme_file:
readme = readme_file.read()
requirements = [
'apistar-jwt==0.4.2',
'apistar-mail==0.3.0',
'apistar==0.5.18',
'bcrypt==3.1.4',
'itsdangerous==0.24',
'logbook==1.3.3',
'psycopg2-binary==2.7.4',
'sqlalchemy==1.2.8',
]
test_requirements = [
'pytest',
'pytest-cov',
'tox'
]
setup(
name='cookie-api',
version='0.1.0',
description='The Cookie API serves as a simple experimentation application for testing concepts in the APIStar '
'platform.',
long_description=readme,
author='Drew Bednar',
author_email='drew@androiddrew.com',
maintainer='Drew Bednar',
maintainer_email='drew@androiddrew.com',
url='https://git.androiddrew.com/androiddrew/cookie-api',
packages=find_packages(exclude=['tests']),
include_package_data=True,
install_requires=requirements,
license='MIT',
zip_safe=False,
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Natural Language :: English',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
],
extras_require={
'testing': test_requirements,
},
entry_points={
'console_scripts': [
'wsgi_runner=wsgi:main'
]
}
)

@ -1,39 +1,37 @@
from apistar.frameworks.wsgi import WSGIApp
from apistar.backends.sqlalchemy_backend import SQLAlchemyBackend, get_session
import pytest import pytest
from sqlalchemy import create_engine
from apistar_jwt import JWT
from cookie_api.app import application_factory
from cookie_api.util import SQLAlchemySession, DBSession
from cookie_api.models import Base from cookie_api.models import Base
from cookie_api.renders import JSONRenderer
from cookie_api.app import commands, routes, components
settings = { test_engine = create_engine('postgresql://apistar:local@localhost/test_cookie_api')
'DATABASE': {
'URL': 'postgresql://apistar:local@localhost/test_cookie_api',
'METADATA': Base.metadata
},
'RENDERERS': [JSONRenderer()],
'JWT': {
'SECRET': 'thisisasecret'
}
}
backend = SQLAlchemyBackend(settings) test_components = [
SQLAlchemySession(engine=test_engine),
JWT({
'JWT_USER_ID': 'sub',
'JWT_SECRET': 'thisisasecret',
}),
]
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def create_db(): def create_db():
Base.metadata.create_all(backend.engine) Base.metadata.create_all(test_engine)
yield yield
Base.metadata.drop_all(backend.engine) Base.metadata.drop_all(test_engine)
@pytest.fixture(name='rb_session') @pytest.fixture(name='rb_session')
def db_session_fixure(): def db_session_fixure():
"Returns a SQLAlchemy session that autorolls back" "Returns a SQLAlchemy session that autorolls back"
session = backend.Session() DBSession.configure(bind=test_engine)
session = DBSession()
try: try:
yield session yield session
session.rollback() session.rollback()
@ -47,7 +45,4 @@ def db_session_fixure():
@pytest.fixture(name='app', scope='session') @pytest.fixture(name='app', scope='session')
def apistar_app_fixture(): def apistar_app_fixture():
"""Returns a session scoped WSGIApp instance""" """Returns a session scoped WSGIApp instance"""
return WSGIApp(settings=settings, return application_factory(components=test_components)
commands=commands,
components=components,
routes=routes)

@ -1,20 +1,25 @@
# CRUD Cookies # CRUD Cookies
import pytest import pytest
import json
from apistar import TestClient from apistar import TestClient
from cookie_api.models import Cookie from cookie_api.models import Cookie
from cookie_api.app import get_cookies, get_cookie from cookie_api.resources.cookies import get_cookies, get_cookie
from cookie_api.util import ExtJSONResponse
def test_get_cookies_empty(app): def test_get_cookies_empty(app):
client = TestClient(app) client = TestClient(app)
response = client.get('/cookies') response = client.get('/cookies')
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == [] assert response.json()['data'] == []
def test_get_empty_cookies(rb_session): def test_get_empty_cookies(rb_session):
assert [] == get_cookies(rb_session) extended_response = get_cookies(rb_session)
assert extended_response.status_code == 200
assert extended_response._content == []
def test_get_cookies(rb_session): def test_get_cookies(rb_session):
@ -26,9 +31,12 @@ def test_get_cookies(rb_session):
rb_session.add(cookie) rb_session.add(cookie)
rb_session.flush() rb_session.flush()
cookies = rb_session.query(Cookie).all() cookies = rb_session.query(Cookie).all()
assert [cookie.to_dict() for cookie in cookies] == get_cookies(rb_session) extended_response = get_cookies(rb_session)
assert len(extended_response._content) == 1
@pytest.mark.skip()
def test_get_cookie(rb_session): def test_get_cookie(rb_session):
cookie = Cookie(name='sugar', cookie = Cookie(name='sugar',
recipe_url='http://cookie.com/sugar', recipe_url='http://cookie.com/sugar',
@ -42,6 +50,7 @@ def test_get_cookie(rb_session):
assert cookie.to_dict() == get_cookie(rb_session, 1) assert cookie.to_dict() == get_cookie(rb_session, 1)
@pytest.mark.skip()
def test_get_cookie_that_doesnt_exist(rb_session): def test_get_cookie_that_doesnt_exist(rb_session):
response = get_cookie(rb_session, 100) response = get_cookie(rb_session, 100)
assert {"error": "404 Not Found"} == response.content assert {"error": "404 Not Found"} == response.content

@ -1,23 +0,0 @@
import datetime as dt
from decimal import Decimal
import json
from cookie_api.renders import extended_encoder, JSONRenderer
def test_extended_encoder_date_parsing():
test_date = dt.datetime(2017, 5, 10)
assert test_date.isoformat() == extended_encoder(test_date)
def test_extended_encoder_decimal_casting():
test_decimal = Decimal('1.0')
assert 1.0 == extended_encoder(test_decimal)
def test_render_with_extended_encoder():
test_date = dt.datetime(2017, 5, 10)
test_decimal = Decimal('0.1')
expected = dict(my_date="2017-05-10T00:00:00", my_float=0.1)
test_response = dict(my_date=test_date, my_float=test_decimal)
assert json.dumps(expected).encode('utf-8') == JSONRenderer().render(test_response)

@ -3,12 +3,33 @@ try:
except ImportError: except ImportError:
# Fall back to psycopg2cffi # Fall back to psycopg2cffi
from psycopg2cffi import compat from psycopg2cffi import compat
compat.register() compat.register()
from apistar_jwt import JWT
from apistar_mail import MailComponent
from sqlalchemy import create_engine
from cookie_api import application_factory from cookie_api import application_factory
from config import settings from cookie_api.util import SQLAlchemySession, SQLAlchemyHook
from config import db_config, jwt_config, mail_config
components = [
SQLAlchemySession(create_engine(db_config)),
JWT(jwt_config),
MailComponent(**mail_config)
]
hooks = [
SQLAlchemyHook()
]
app = application_factory(components=components, hooks=hooks)
def main():
app.serve('0.0.0.0', 5000, debug=True)
app = application_factory(settings)
if __name__ == "__main__": if __name__ == '__main__':
app.main() main()
Loading…
Cancel
Save