Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions adaptable/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""adaptable.

A serialization middleware.
"""
from abc import abstractmethod


class _AdapterRegistry(type):
"""Adapter regsitry.

Attributes:
adapters (dict): Name, object pairs.
"""

adapters = {}

def __new__(cls, name, bases, attrs):
new_cls = type.__new__(cls, name, bases, attrs)
cls.adapters[name] = new_cls
return new_cls


class Adapter(metaclass=_AdapterRegistry):
"""Serialization abstraction layer.

"deserialize" accepts an unvalidated, raw dictionary and returns a
validated dictionary. It will likely implement your "view"
attribute's deserialization functionality. This method can be as
simple as calling your view or can require complex field marshaling
and schema generation. The method should accept any number of
keyword arguments that are necessary to complete the operation.

"serialize" accepts a model instance and returns serialized output.
Complex marshaling and schema generation can be applied here. This
method should only ever accept one required "model" argument.

"fetch_all" should return a list of model instances that were
retrieved by a "select" query operation.

"fetch_one" should return a model instance that was retrieved by a
"select" query operation.

"make_query" should accept any number of keyword arguments and
apply those keyword arguments as query filters. The returned object
should be a callable capable of communicating with a database and
returning the desired output.

Attributes:
model (object): Model object type.
view (object): Schema object type.
"""

model = None
view = None

@abstractmethod
def deserialize(self, form: dict) -> dict:
"""Return a deserialized dictionary.

Keyword arguments:
form (dict): Dictionary.
"""
return

def deserialize_all(self, forms: list) -> list:
"""Return a set of deserialized dictionaries.

Keyword arguments:
forms (list): List of dictionaries.
"""
return [self.deserialize(form) for form in forms]

@abstractmethod
def serialize(self, model) -> dict:
"""Return a serialized model object.

Keyword arguments:
model: Model class instance.
"""
return

def serialize_all(self, models: list) -> list:
"""Return a set of serialized models.

Keyword arguments:
models (list): List of model class instances.
"""
return [self.serialize(model) for model in models]

def fetch(self, id, **kwargs):
"""Return a model instance.

A query is constructed from the "make_query" method prior to
fetching the model instance.

Keyword arguments:
id: Unique model identifier.
"""
query = self.make_query(**kwargs)
return self.fetch_one(query, id)

@abstractmethod
def fetch_compounded(self, id, **kwargs):
"""Return a single-object query including joined relationships.

Keyword arguments:
id: Unique model identifier.
filters (dict): Column, value filter options.
"""
return

@abstractmethod
def fetch_all(self, query) -> list:
"""Return a list of model instances.

Keyword arguments:
query: ORM query object.
"""
return

@abstractmethod
def fetch_one(self, query, id):
"""Return a model instance or None.

Keyword arguments:
query: ORM query object.
id: Unique model identifier.
"""
return

@abstractmethod
def make_query(self):
"""Return a query object."""
return

@abstractmethod
def make_models_response(self, models: list):
"""Return a serialized set of model instances.

Keyword arguments:
models (list): List of model class instances.
"""
return

@abstractmethod
def make_model_response(self, model):
"""Return a serialized model instance.

Keyword arguments:
model: Model class instance.
"""
return

@abstractmethod
def make_collection_response(self, **filters):
"""Return a collection query response type.

Keyword arguments:
filters (dict): Column, value filter options.
"""
return

@abstractmethod
def make_single_object_response(self, id, **filters):
"""Return a single-object query response type.

Keyword arguments:
id: Unique model identifier.
filters (dict): Column, value filter options.
"""
return


def get_adapter(name: str) -> Adapter:
"""Return the requested class or raise.

Keyword arguments:
name (str): Adapter class name.
"""
return _AdapterRegistry.adapters[name]
142 changes: 142 additions & 0 deletions adaptable/extensions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Adapter extensions.

Extensions allow for out-of-the-box compatibility with your API
specification of choice.

Currently supported formats:
- JSONAPI (http://jsonapi.org)
"""
from abc import abstractmethod

from jsonapiquery import JSONAPIQuery
from jsonapiquery.database.sqlalchemy import group_and_remove

from adaptable import Adapter


class JSONAPIAdapter(Adapter):
"""JSONAPI adapter extension.

Attributes:
query_options (dict): Option, boolean pairs.
"""

query_options = {}
"""JSONAPI query options.

Setting the "can_filter", "can_sort", "can_paginate", and "can_compound"
keys with a value of `False` will prevent the adapter from allowing those
behaviors to occur.

"can_*" values default to True but they can specified explicitly if
required.
"""

def __init__(self, base_url='', parameters={}):
"""Initialize the adapter class.

Keyword arguments:
base_url (str): Request URL stripped of its query parameters.
parameters (dict): Parameter, value pairs.
"""
self.base_url = base_url
self.parameters = parameters

def make_jsonapi(self, model, view) -> JSONAPIQuery:
"""Return a JSONAPI instance.

Keyword arguments:
model (object): Model object type.
view (object): Schema object type.
"""
return JSONAPIQuery(self.parameters, model, view)

@abstractmethod
def make_jsonapi_errors(self, errors: list):
"""Return a JSONAPI error class instance.

Keyword arguments:
errors (list): A list of JSONAPI errors.
"""
return

def make_models_response(self, models: list) -> dict:
"""Return a set of JSONAPI serialized models.

Keyword arguments:
models (list): List of model class instances.
"""
return {'data': self.serialize_all(models)}

def make_model_response(self, model) -> dict:
"""Return a JSONAPI serialized model.

Keyword arguments:
model: Model class instance.
"""
return {'data': self.serialize(model)}

def make_collection_response(self, **kwargs) -> dict:
"""Return a JSONAPI formatted collection response.

If configured through the "query_options" attribute, this
method may filter, sort, compound and paginate the document.

Keyword arguments:
kwargs (dict): Dictionary of query restrictions.
"""
jsonapi = self.make_jsonapi(self.model, self.view)
query, total, selects, schemas = jsonapi.make_query(
self.make_query(**kwargs), self.query_options)

models = self.fetch_all(query)
models = group_and_remove(models, selects + [self.model])

response = self.make_models_response(models.pop())
response = jsonapi.make_included_response(response, models, schemas)
response = jsonapi.make_paginated_response(
response, self.base_url, total)
return response

def make_single_object_response(self, id, **kwargs) -> dict:
"""Return a JSONAPI formatted response object.

If configured through the "query_options" attribute, the
document may be compounded.

Keyword arguments:
id: Unique model identifier.
kwargs (dict): Dictionary of query restrictions.
"""
model, includes, schemas = self.fetch_compounded(id, **kwargs)
jsonapi = self.make_jsonapi(self.model, self.view)

response = self.make_model_response(model)
response = jsonapi.make_included_response(response, includes, schemas)
return response

def fetch_compounded(self, id, **kwargs):
"""Return a model and the requested joins.

If configured through the "query_options" attribute, the
query may join and return related models.

Keyword arguments:
id: Unique model identifier.
kwargs (dict): Dictionary of query restrictions.
"""
query = self.make_query(**kwargs)
jsonapi = self.make_jsonapi(self.model, self.view)

selects = []
schemas = []
if self.query_options.get('can_compound', True):
query, selects, schemas, errors = jsonapi.include(query)
if errors:
raise self.make_jsonapi_errors(errors)

result = self.fetch_one(query, id)
if isinstance(result, tuple):
includes = group_and_remove([result[1:]], selects)
return result[0], includes, schemas
return result, [], []
33 changes: 33 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""adaptable.

A serialization middleware.
"""
from setuptools import find_packages, setup


setup(
name='adaptable',
version='0.1',
url='https://github.com/caxiam/adaptable',
license='Apache Version 2.0',
author='Colton Allen',
author_email='colton.allen@caxiam.com',
description='A convention driven approach to serialization.',
long_description=__doc__,
packages=find_packages(exclude=("tests*", "docs*")),
package_dir={'adaptable': 'adaptable'},
zip_safe=False,
include_package_data=True,
platforms='any',
install_requires=[],
classifiers=[
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development :: Libraries :: Python Modules'
],
test_suite='tests'
)
Empty file added tests/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions tests/jsonapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from jsonapiquery import JSONAPIQuery
from jsonapiquery.drivers.model import SQLAlchemyDriver
from jsonapiquery.drivers.view import MarshmallowDriver


class JSONAPI(JSONAPIQuery):
model_driver = SQLAlchemyDriver
view_driver = MarshmallowDriver

def make_errors(self, errors):
"""Return an "Exception" instance."""
return Exception(errors)

def serialize_included(self, schema, models):
"""Return serialized output.

For included responses, we only want to include "id" and "type"
fields. This method of inclusion is obviously contrived. More
flexibility can be found by offloading this hard coded behavior
to the schema. By using a request context, in the case of flask,
the method can key off the request URL and return an adapter
based on the API being accessed.
"""
adapter = schema.get_adapter(simplified=True)()
return adapter.serialize_all(models)
Loading