Commit 74134456 authored by Ashish Gore's avatar Ashish Gore

Merge pull request #1 from dealertrack/data-api

Data API and bug fixes
parents 60ed061d 6cadf669
......@@ -3,6 +3,7 @@ include CONTRIBUTING.rst
include HISTORY.rst
include LICENSE
include README.rst
include requirements.txt
recursive-include tests *
recursive-exclude * __pycache__
......
"""
API Builder
Build dynamic API based on the provided SQLAlchemy model
"""
from managers import AlchemyModelManager
from viewsets import AlchemyModelViewSet
from routers import ReadOnlyRouter
class APIModelBuilder(object):
def __init__(self,
models,
base_managers,
base_viewsets=None,
*args, **kwargs):
self.models = models
if not isinstance(base_managers, (tuple, list)):
base_managers = [base_managers]
if not isinstance(base_managers, tuple):
base_managers = tuple(base_managers)
if base_viewsets is None:
base_viewsets = (AlchemyModelViewSet,)
self.base_managers = base_managers
self.base_viewsets = base_viewsets
@property
def urls(self):
router = ReadOnlyRouter()
for model in self.models:
manager = type(
str('{}Manager'.format(model.__name__)),
self.base_managers + (AlchemyModelManager,),
{
'model_class': model,
}
)
viewset = type(
str('{}ModelViewSet'.format(model.__name__)),
self.base_viewsets,
{
'manager_class': manager,
'paginate_by': 10,
}
)
router.register('data-api/' + model.__name__.lower() + 's',
viewset,
base_name=model.__name__)
return router.urls
......@@ -28,7 +28,7 @@ class AlchemyModelManager(object):
try:
pk = primary_key(self.cls)
except KeyNotFoundException:
return list()
pk = None
filter_dict = dict()
......@@ -48,14 +48,28 @@ class AlchemyModelManager(object):
query_pks[key] = other_pks[key]
query_pks.update(filter_dict)
queryset = self.session.query(self.cls.__dict__[pk]).filter_by(
**query_pks).all()
if pk:
queryset = self.session.query(self.cls.__dict__[pk]).filter_by(
**query_pks).all()
else:
queryset = self.session.query(self.cls).filter_by(
**query_pks).all()
else:
if filter_dict:
queryset = self.session.query(self.cls.__dict__[pk]).filter_by(
**filter_dict).all()
if pk:
queryset = self.session.query(
self.cls.__dict__[pk]).filter_by(
**filter_dict).all()
else:
queryset = self.session.query(self.cls).filter_by(
**filter_dict).all()
else:
queryset = self.session.query(self.cls.__dict__[pk]).all()
if pk:
queryset = self.session.query(self.cls.__dict__[pk]).all()
else:
# Limit to 1000 rows, this is worst case scenario
queryset = self.session.query(self.cls).limit(1000).all()
return queryset
......
import importlib
import inspect
import itertools
import os
import six
from django.conf import settings
def module_walk(root_module, include_self=True):
if isinstance(root_module, six.string_types):
root_module = importlib.import_module(root_module)
if include_self:
yield root_module
root_path = os.path.abspath(root_module.__file__)
if '__init__' not in os.path.basename(root_path):
return
root_path = os.path.dirname(root_path)
for path, dirs, files in os.walk(root_path):
path = os.path.abspath(path)
for file in files:
if not file.rsplit('.', 1)[-1] == 'py':
continue
if file == '__init__.py':
file = ''
file = os.path.join(path, file)
file = file.replace(root_path + os.sep, '')
file = file.rsplit('.', 1)[0]
if '.' in file:
continue
module_path = '.'.join(filter(
None,
root_module.__name__.split('.')
+ file.split(os.sep)
))
module = importlib.import_module(module_path)
yield module
class ModelCache(object):
"""
Store for all the models in the project
Based on Django's AppCache, this collects all the
models in the application for introspection.
Future:
There will be a settings.py file which will allow
customization
"""
__shared_state = dict(
modules={},
)
def __init__(self):
self.__dict__ = self.__shared_state
@property
def models(self):
if hasattr(self, '_models'):
return self._models
self._models = list(set(itertools.chain(
*self.modules.values()
)))
return self._models
def find_models_in_module(self, module_path):
for module in module_walk(module_path, include_self=True):
models = self._get_models_from_module(module)
if models:
self.modules[module.__name__] = models
def _get_models_from_module(self, module):
models = [
obj for name, obj in vars(module).items()
if (inspect.isclass(obj)
and hasattr(obj, '_sa_class_manager'))
]
return models
model_cache = ModelCache()
for path in getattr(settings, 'SA_MODEL_LOADER', []):
model_cache.find_models_in_module(path)
......@@ -4,10 +4,10 @@ SQLALchemy and DRF fields to serialize/deserialize objects
'''
from rest_framework import serializers
from rest_framework.fields import (CharField, IntegerField, DateTimeField,
FloatField, BooleanField)
FloatField, BooleanField, DecimalField)
from sqlalchemy.types import (String, INTEGER, SMALLINT, BIGINT, VARCHAR,
CHAR, TIMESTAMP, DATE, Float, BigInteger,
Numeric, DateTime, Boolean, CLOB)
Numeric, DateTime, Boolean, CLOB, DECIMAL)
from django.utils.datastructures import SortedDict
from djangorest_alchemy.fields import AlchemyRelatedField, AlchemyUriField
# inspect introduced in 0.8
......@@ -36,12 +36,15 @@ class AlchemyModelSerializer(serializers.Serializer):
Numeric: IntegerField,
DateTime: DateTimeField,
Boolean: BooleanField,
CLOB: CharField
CLOB: CharField,
DECIMAL: DecimalField,
}
def __init__(self, *args, **kwargs):
assert "model_class" in kwargs, \
"model_class should be passed"
assert 'request' in kwargs['context'], \
"Context must contain request object"
self.cls = kwargs.pop('model_class')
super(AlchemyModelSerializer, self).__init__(*args, **kwargs)
......@@ -52,15 +55,14 @@ class AlchemyModelSerializer(serializers.Serializer):
mapper = class_mapper(self.cls.__class__)
r = self.context['request']
try:
# URI field for get pk field
pk_field = primary_key(self.cls.__class__)
ret['href'] = AlchemyUriField(source=pk_field,
path=r.build_absolute_uri(r.path))
except KeyNotFoundException:
return ret
r = self.context['request']
ret['href'] = AlchemyUriField(source=pk_field,
path=r.build_absolute_uri(r.path))
pass
# Get all the Column fields
for col_prop in mapper.iterate_properties:
......@@ -94,12 +96,12 @@ class AlchemyListSerializer(AlchemyModelSerializer):
try:
# URI field for get pk field
pk_field = primary_key(self.cls.__class__)
except KeyNotFoundException:
return ret
request = self.context['request']
ret["href"] = AlchemyUriField(source=pk_field,
path=request.build_absolute_uri(
request.path))
request = self.context['request']
ret["href"] = AlchemyUriField(source=pk_field,
path=request.build_absolute_uri
(request.path))
except KeyNotFoundException:
return super(AlchemyListSerializer, self).get_default_fields()
return ret
import unittest
from djangorest_alchemy.apibuilder import APIModelBuilder
import mock
class TestAPIBuilder(unittest.TestCase):
def test_urls(self):
"""
Test basic urls property
"""
class Model(object):
pass
class Model2(object):
pass
class SessionMixin(object):
def __init__(self):
self.session = mock.Mock()
builder = APIModelBuilder([Model, Model2], SessionMixin)
self.assertIsNotNone(builder.urls)
......@@ -22,6 +22,8 @@ from rest_framework.decorators import action
RESULTS_KEY = "results"
COUNT_KEY = "count"
PAGE_KEY = "page"
class PrimaryKeyMixin(object):
......@@ -104,6 +106,8 @@ class TestAlchemyViewSetIntegration(TestCase):
self.assertTrue(resp.status_code is status.HTTP_200_OK)
self.assertTrue(type(resp.data) is dict)
self.assertTrue(len(resp.data[RESULTS_KEY]) == 1)
self.assertTrue(resp.data[COUNT_KEY] == 1)
self.assertTrue(resp.data[PAGE_KEY] == 25)
def test_decl_retrieve(self):
resp = self.client.get('/api/declmodels/1/')
......
......@@ -79,12 +79,13 @@ class AlchemyModelViewSet(MultipleObjectMixin, ManagerMixin, viewsets.ViewSet):
* URI contains the same pk field
* Complete URI with server/port is returned back
'''
mgr = self.manager_factory(context={'request': request})
queryset = mgr.list(other_pks=self.get_other_pks(request),
filters=request.QUERY_PARAMS)
count = len(queryset)
if self.paginate_by:
try:
queryset = self.get_page(queryset)
......@@ -95,7 +96,9 @@ class AlchemyModelViewSet(MultipleObjectMixin, ManagerMixin, viewsets.ViewSet):
mgr.model_class(),
{'request': request})
return Response({"results": serializer.data})
return Response({"count": count,
"page": self.paginate_by,
"results": serializer.data})
def retrieve(self, request, **kwargs):
'''
......
......@@ -3,6 +3,8 @@ import os
import sys
if __name__ == "__main__":
sys.path.append(os.path.abspath(os.path.join(__file__, '..')))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
from django.core.management import execute_from_command_line
......
......@@ -123,6 +123,8 @@ INSTALLED_APPS = (
# 'django.contrib.admindocs',
)
SA_MODEL_LOADER = ('models',)
# DJANGO REST FRAMEWORK SETTINGS
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
......
......@@ -3,10 +3,15 @@ Url registrations
'''
from django.conf.urls import patterns, include, url
from rest_framework_nested import routers
from djangorest_alchemy.apibuilder import APIModelBuilder
from djangorest_alchemy.managers import AlchemyModelManager
from viewsets import CarViewSet
from viewsets import PartViewSet
from models import SessionMixin
viewset_router = routers.SimpleRouter()
viewset_router.register(r'api/cars', CarViewSet,
base_name='car')
......@@ -17,7 +22,14 @@ child_router = routers.NestedSimpleRouter(viewset_router, r'api/cars',
child_router.register("parts", PartViewSet,
base_name='part')
# Demonstrate dynamic API builder featire
from djangorest_alchemy.model_cache import model_cache
builder = APIModelBuilder(model_cache.models,
(SessionMixin, AlchemyModelManager))
urlpatterns = patterns('',
url(r'^', include(viewset_router.urls)),
url(r'^', include(child_router.urls)),
url(r'^', include(builder.urls)),
)
......@@ -12,8 +12,6 @@ flake8==2.1.0
mccabe==0.2.1
mock==1.0.1
nose==1.3.2
pep8==1.4.6
pyflakes==0.7.3
six==1.7.2
wsgiref==0.1.2
sphinx-rtd-theme==0.1.6
......@@ -19,7 +19,7 @@ history = open('HISTORY.rst').read().replace('.. :changelog:', '')
setup(
name='djangorest-alchemy',
version='0.1.0',
version='0.1.3',
description='Django REST Framework and SQLAlchemy integration',
long_description=readme + '\n\n' + history,
author='Ashish Gore',
......@@ -31,8 +31,7 @@ setup(
package_dir={'djangorest_alchemy':
'djangorest_alchemy'},
include_package_data=True,
install_requires=[
],
install_requires=open("requirements.txt").readlines(),
license="BSD",
zip_safe=False,
keywords='djangorest-alchemy',
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment