Leveled up a lot of code. Should have committed more frequently
parent
e3e4ba7ace
commit
4f4099d998
@ -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]
|
||||||
|
py
|
||||||
|
```
|
@ -0,0 +1,5 @@
|
|||||||
|
from .app import MetaApp
|
||||||
|
from .http import MetaJSONResponse, ExtJSONResponse
|
||||||
|
from .component import SQLAlchemySession
|
||||||
|
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,84 @@
|
|||||||
|
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 = 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,40 @@
|
|||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open('README.md') as readme_file:
|
||||||
|
readme = readme_file.read()
|
||||||
|
|
||||||
|
with open('requirements.txt') as requirements_file:
|
||||||
|
requirements = requirements_file.read()
|
||||||
|
|
||||||
|
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',
|
||||||
|
url='https://git.androiddrew.com/androiddrew/cookie-api',
|
||||||
|
packages=find_packages(include=['cookie_api']),
|
||||||
|
include_package_data=True,
|
||||||
|
install_requires=requirements,
|
||||||
|
license='MIT',
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
@ -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)
|
|
Loading…
Reference in New Issue