Skip to content
This repository has been archived by the owner on Nov 28, 2021. It is now read-only.

Commit

Permalink
Merge pull request #174 from anomaly/2.3.0-rc
Browse files Browse the repository at this point in the history
2.3.0
  • Loading branch information
BradMclain authored Aug 29, 2018
2 parents a2e16b5 + c3a085d commit 3712d4a
Show file tree
Hide file tree
Showing 24 changed files with 1,127 additions and 509 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
test:
python setup.py test

.PHONY: tests-prep
tests-prep:
pip install tox tox-pyenv
pyenv local 2.7.15 3.5.6 3.6.6 pypy2.7-6.0.0

.PHONY: tests
tests:
tox
Expand Down
2 changes: 1 addition & 1 deletion prestans/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@

__all__ = ['http', 'types', 'rest', 'parser', 'serializer', 'deserializer', 'provider', 'ext', 'exception']

__version_info__ = (2, 2, 1)
__version_info__ = (2, 3, 0)
__version__ = '.'.join(str(v) for v in __version_info__)
23 changes: 22 additions & 1 deletion prestans/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ class AttributeFilterDiffers(RequestException):
def __init__(self, attribute_list):

_code = STATUS.BAD_REQUEST
_message = "attribute filter does not contain attributes (%s) that are not part of template" % (
_message = "attribute filter contains attributes (%s) that are not part of template" % (
', '.join(attribute_list)
)

Expand Down Expand Up @@ -495,6 +495,27 @@ def __init__(self, message="Moved Permanently", response_model=None):
super(MovedPermanently, self).__init__(_code, message, response_model)


class Redirect(ResponseException):

def __init__(self, code, url):
self._url = url
super(Redirect, self).__init__(code, "Redirect")

@property
def url(self):
return self._url


class TemporaryRedirect(Redirect):
def __init__(self, url):
super(TemporaryRedirect, self).__init__(STATUS.TEMPORARY_REDIRECT, url)


class PermanentRedirect(Redirect):
def __init__(self, url):
super(PermanentRedirect, self).__init__(STATUS.PERMANENT_REDIRECT, url)


class PaymentRequired(ResponseException):

def __init__(self, message="Payment Required", response_model=None):
Expand Down
235 changes: 220 additions & 15 deletions prestans/ext/data/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,20 @@
#
import inspect

from prestans.types import Model
from prestans import exception
from prestans import parser
from prestans import types


class ModelAdapter(object):

def __init__(self, rest_model_class, persistent_model_class):
"""
:param rest_model_class:
:param persistent_model_class:
"""

if issubclass(rest_model_class, Model):
if issubclass(rest_model_class, types.Model):
self._rest_model_class = rest_model_class
else:
raise TypeError("rest_model_class must be sub class of prestans.types.Model")
Expand All @@ -52,18 +58,184 @@ def persistent_model_class(self):
@property
def rest_model_class(self):
return self._rest_model_class

def adapt_persistent_to_rest(self, persistent_object):
raise NotImplementedError("adapt_persistent_to_rest direct use not allowed")

def adapt_persistent_to_rest(self, persistent_object, attribute_filter=None):
"""
adapts a persistent model to a rest model by inspecting
"""

rest_model_instance = self.rest_model_class()

for attribute_key in rest_model_instance.get_attribute_keys():

rest_attr = getattr(self.rest_model_class, attribute_key)

# don't bother processing if the persistent model doesn't have this attribute
if not hasattr(persistent_object, attribute_key):

if isinstance(rest_attr, types.Model):
#: If the attribute is a Model, then we set it to None otherwise we get a model
#: with default values, which is invalid when constructing responses
try:
setattr(rest_model_instance, attribute_key, None)
# catch any exception thrown from setattr to give a usable error message
except TypeError as exp:
raise TypeError('Attribute %s, %s' % (attribute_key, str(exp)))

continue
# ignore class methods
elif inspect.ismethod(getattr(persistent_object, attribute_key)):
import logging
logging.error("ignoring method: "+attribute_key)
continue
# attribute is not visible don't bother processing
elif isinstance(attribute_filter, parser.AttributeFilter) and \
not attribute_filter.is_attribute_visible(attribute_key):
continue

# handles prestans array population from SQLAlchemy relationships
elif isinstance(rest_attr, types.Array):

persistent_attr_value = getattr(persistent_object, attribute_key)
rest_model_array_handle = getattr(rest_model_instance, attribute_key)

# iterator uses the .append method exposed by prestans arrays to validate
# and populate the collection in the instance.
for collection_element in persistent_attr_value:
if isinstance(rest_attr.element_template, types.Boolean) or \
isinstance(rest_attr.element_template, types.Float) or \
isinstance(rest_attr.element_template, types.Integer) or \
isinstance(rest_attr.element_template, types.String):
rest_model_array_handle.append(collection_element)
else:
element_adapter = registry.get_adapter_for_rest_model(rest_attr.element_template)

# check if there is a sub model filter
sub_attribute_filter = None
if attribute_filter and attribute_key in attribute_filter:
sub_attribute_filter = getattr(attribute_filter, attribute_key)

adapted_rest_model = element_adapter.adapt_persistent_to_rest(
collection_element,
sub_attribute_filter
)
rest_model_array_handle.append(adapted_rest_model)

elif isinstance(rest_attr, types.Model):

try:
persistent_attr_value = getattr(persistent_object, attribute_key)

if persistent_attr_value is None:
adapted_rest_model = None
else:
model_adapter = registry.get_adapter_for_rest_model(rest_attr)

# check if there is a sub model filter
sub_attribute_filter = None
if isinstance(attribute_filter, parser.AttributeFilter) and \
attribute_key in attribute_filter:
sub_attribute_filter = getattr(attribute_filter, attribute_key)

adapted_rest_model = model_adapter.adapt_persistent_to_rest(
persistent_attr_value,
sub_attribute_filter
)

setattr(rest_model_instance, attribute_key, adapted_rest_model)

except TypeError as exp:
raise TypeError('Attribute %s, %s' % (attribute_key, str(exp)))
except exception.DataValidationException as exp:
raise exception.InconsistentPersistentDataError(attribute_key, str(exp))

else:

# otherwise copy the value to the rest model
try:
persistent_attr_value = getattr(persistent_object, attribute_key)
setattr(rest_model_instance, attribute_key, persistent_attr_value)
except TypeError as exp:
raise TypeError('Attribute %s, %s' % (attribute_key, str(exp)))
except exception.ValidationError as exp:
raise exception.InconsistentPersistentDataError(attribute_key, str(exp))

return rest_model_instance


def adapt_persistent_instance(persistent_object, target_rest_class=None, attribute_filter=None):
"""
Adapts a single persistent instance to a REST model; at present this is a
common method for all persistent backends.
Refer to: https://groups.google.com/forum/#!topic/prestans-discuss/dO1yx8f60as
for discussion on this feature
"""

# try and get the adapter and the REST class for the persistent object
if target_rest_class is None:
adapter_instance = registry.get_adapter_for_persistent_model(persistent_object)
else:
if inspect.isclass(target_rest_class):
target_rest_class = target_rest_class()

adapter_instance = registry.get_adapter_for_persistent_model(persistent_object, target_rest_class)

# would raise an exception if the attribute_filter differs from the target_rest_class
if attribute_filter is not None and isinstance(attribute_filter, parser.AttributeFilter):
parser.AttributeFilter.from_model(target_rest_class).conforms_to_template_filter(attribute_filter)

return adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter)


def adapt_persistent_collection(persistent_collection, target_rest_class=None, attribute_filter=None):
# ensure that collection is iterable and has at least one element
persistent_collection_length = 0

# attempt to detect the length of the persistent_collection
if persistent_collection and isinstance(persistent_collection, (list, tuple)):
persistent_collection_length = len(persistent_collection)
# SQLAlchemy query
elif persistent_collection and persistent_collection.__module__ == "sqlalchemy.orm.query":
persistent_collection_length = persistent_collection.count()
# Google App Engine NDB
elif persistent_collection and persistent_collection.__module__ == "google.appengine.ext.ndb":
persistent_collection_length = persistent_collection.count()

# if the persistent_collection is empty then return a blank array
if persistent_collection_length == 0:
return types.Array(element_template=target_rest_class())

# try and get the adapter and the REST class for the persistent object
if target_rest_class is None:
adapter_instance = registry.get_adapter_for_persistent_model(
persistent_collection[0]
)
else:
if inspect.isclass(target_rest_class):
target_rest_class = target_rest_class()

adapter_instance = registry.get_adapter_for_persistent_model(
persistent_collection[0],
target_rest_class
)

adapted_models = types.Array(element_template=adapter_instance.rest_model_class())

for persistent_object in persistent_collection:
adapted_models.append(adapter_instance.adapt_persistent_to_rest(persistent_object, attribute_filter))

return adapted_models


class AdapterRegistryManager(object):
"""
AdapterRegistryManager keeps track of rest to persistent model maps
AdapterRegistryManager should not be instantiated by the applications, a singleton
instance supplied by this package.
New AdapterRegistryManager's should not be instantiated by the application, a singleton
instance is supplied by this package.
"""
DEFAULT_REST_ADAPTER = "prestans_rest_default_adapter"

def __init__(self):
self._persistent_map = dict()
Expand All @@ -85,22 +257,55 @@ def register_adapter(self, model_adapter):
rest_class_signature = self.generate_signature(model_adapter.rest_model_class)
persistent_class_signature = self.generate_signature(model_adapter.persistent_model_class)

# store references to how a rest model maps to a persistent model and vice versa
self._persistent_map[persistent_class_signature] = model_adapter
# store references to rest model
self._rest_map[rest_class_signature] = model_adapter

def get_adapter_for_persistent_model(self, persistent_model):

if persistent_class_signature not in self._persistent_map:
self._persistent_map[persistent_class_signature] = dict()

# store a reference to the adapter under both REST signature and default key
# the default is always the last registered model (to match behaviour before this was patched)
self._persistent_map[persistent_class_signature][self.DEFAULT_REST_ADAPTER] = model_adapter
self._persistent_map[persistent_class_signature][rest_class_signature] = model_adapter

def register_persistent_rest_pair(self, persistent_model_class, rest_model_class):
"""
:param persistent_model_class:
:param rest_model_class:
"""
self.register_adapter(ModelAdapter(
rest_model_class=rest_model_class,
persistent_model_class=persistent_model_class
))

def clear_registered_adapters(self):
"""
Clears all of the currently registered model adapters
"""
self._persistent_map.clear()
self._rest_map.clear()

def get_adapter_for_persistent_model(self, persistent_model, rest_model=None):
"""
:param persistent_model: instance of persistent model
:param rest_model: specific REST model
:return: the matching model adapter
:rtype: ModelAdapter
"""
class_signature = self.generate_signature(persistent_model)
persistent_signature = self.generate_signature(persistent_model)

if class_signature not in self._persistent_map:
raise TypeError("No registered Data Adapter for class %s" % class_signature)
if persistent_signature in self._persistent_map:
sub_map = self._persistent_map[persistent_signature]

# return the first match if REST model was not specified
if rest_model is None:
return self._persistent_map[persistent_signature][self.DEFAULT_REST_ADAPTER]
else:
rest_sig = self.generate_signature(rest_model)
if rest_sig in sub_map:
return self._persistent_map[persistent_signature][rest_sig]

return self._persistent_map[class_signature]
raise TypeError("No registered Data Adapter for class %s" % persistent_signature)

def get_adapter_for_rest_model(self, rest_model):
"""
Expand Down
Loading

0 comments on commit 3712d4a

Please sign in to comment.