├── testapp ├── tests.py ├── __init__.py ├── admin.py ├── views.py ├── migrations │ ├── __init__.py │ └── 0001_initial.py ├── urls.py ├── resources.py └── models.py ├── django_ripozo ├── views.py ├── admin.py ├── tests.py ├── migrations │ └── __init__.py ├── models.py ├── __init__.py ├── exceptions.py ├── easy_resources.py ├── manager.py └── dispatcher.py ├── test_project ├── __init__.py ├── urls.py ├── wsgi.py └── settings.py ├── examples ├── todolist │ ├── todoapp │ │ ├── __init__.py │ │ ├── migrations │ │ │ ├── __init__.py │ │ │ └── 0001_initial.py │ │ ├── tests.py │ │ ├── admin.py │ │ ├── views.py │ │ ├── urls.py │ │ ├── models.py │ │ ├── managers.py │ │ └── resources.py │ ├── todolist │ │ ├── __init__.py │ │ ├── urls.py │ │ ├── wsgi.py │ │ └── settings.py │ └── manage.py └── django_example_requests.py ├── MANIFEST.in ├── django_ripozo_tests ├── integration │ ├── __init__.py │ ├── basic.py │ ├── dispatcher.py │ ├── easy_resources.py │ └── method_router.py ├── helpers │ ├── __init__.py │ ├── django_settings.py │ └── common.py ├── unit │ ├── __init__.py │ ├── dispatcher.py │ ├── onetomanymanager.py │ ├── method_router.py │ └── manager.py └── __init__.py ├── manage.py ├── docs ├── source │ ├── API.rst │ ├── index.rst │ ├── tutorial │ │ └── setup.rst │ └── conf.py ├── Makefile └── make.bat ├── tox.ini ├── .travis.yml ├── CHANGELOG.rst ├── .gitignore ├── setup.py ├── README.rst └── LICENSE.txt /testapp/tests.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_ripozo/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_ripozo/admin.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_ripozo/tests.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_ripozo/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/todolist/todolist/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE.txt 3 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /testapp/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /testapp/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /django_ripozo/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /testapp/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | -------------------------------------------------------------------------------- /django_ripozo_tests/integration/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from . import basic 9 | -------------------------------------------------------------------------------- /django_ripozo_tests/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from . import common, django_settings 9 | -------------------------------------------------------------------------------- /django_ripozo_tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from . import manager, onetomanymanager, dispatcher, method_router 9 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | # Examples: 6 | # url(r'^$', 'test_app.views.home', name='home'), 7 | # url(r'^blog/', include('blog.urls')), 8 | 9 | url(r'^admin/', include(admin.site.urls)), 10 | ] 11 | -------------------------------------------------------------------------------- /examples/todolist/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todolist.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /django_ripozo/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django_ripozo.manager import DjangoManager 7 | from django_ripozo.dispatcher import DjangoDispatcher 8 | from django_ripozo.dispatcher import DjangoRequestContainer 9 | -------------------------------------------------------------------------------- /docs/source/API.rst: -------------------------------------------------------------------------------- 1 | djanog-ripozo API 2 | ================= 3 | 4 | Dispatcher 5 | ---------- 6 | 7 | .. automodule:: django_ripozo.dispatcher 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | Manager 13 | ------- 14 | 15 | .. automodule:: django_ripozo.manager 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | -------------------------------------------------------------------------------- /examples/todolist/todolist/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | from django.contrib import admin 3 | 4 | urlpatterns = [ 5 | # Examples: 6 | # url(r'^$', 'todolist.views.home', name='home'), 7 | # url(r'^blog/', include('blog.urls')), 8 | 9 | url(r'', include('todoapp.urls')), 10 | url(r'^admin/', include(admin.site.urls)), 11 | ] 12 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/urls.py: -------------------------------------------------------------------------------- 1 | from django_ripozo import DjangoDispatcher 2 | from ripozo.adapters import SirenAdapter, HalAdapter 3 | from .resources import TaskBoardResource, TaskResource 4 | 5 | dispatcher = DjangoDispatcher(base_url='/api') 6 | dispatcher.register_resources(TaskBoardResource, TaskResource) 7 | dispatcher.register_adapters(SirenAdapter, HalAdapter) 8 | 9 | urlpatterns = dispatcher.url_patterns 10 | -------------------------------------------------------------------------------- /django_ripozo_tests/helpers/django_settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | SECRET_KEY = 'not a secret' 7 | 8 | DATABASES = { 9 | 'default': { 10 | 'ENGINE': 'django.db.backends.sqlite3', 11 | 'NAME': ':memory:' 12 | } 13 | } 14 | 15 | INSTALLED_APPS = ( 16 | 'manager', 17 | ) -------------------------------------------------------------------------------- /examples/todolist/todoapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | 5 | class TaskBoard(models.Model): 6 | title = models.CharField(max_length=50) 7 | 8 | 9 | class Task(models.Model): 10 | title = models.CharField(max_length=50) 11 | description = models.TextField() 12 | completed = models.BooleanField(default=False) 13 | task_board = models.ForeignKey('TaskBoard', related_name='task_set') 14 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py{27,33,34,py,py3}-django{16,17,18,dev}-ripozo{release,git} 3 | 4 | [testenv] 5 | deps= 6 | django16: Django>=1.6,<1.7 7 | django17: Django>=1.7,<1.8 8 | django18: Django>=1.8,<1.9 9 | djangodev: git+git://github.com/django/django.git@master 10 | ripozorelease: ripozo 11 | ripozogit: git+git://github.com/vertical-knowledge/ripozo.git@master 12 | commands= 13 | python setup.py install 14 | python setup.py test -------------------------------------------------------------------------------- /test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_app project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /examples/todolist/todolist/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for todolist project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "todolist.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /django_ripozo/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.exceptions import RestException 7 | 8 | 9 | class MethodNotAllowed(RestException): 10 | """ 11 | Raised when an unavailable http 12 | verb is used. 13 | """ 14 | def __init__(self, message=None, status_code=405): 15 | super(MethodNotAllowed, self).__init__(message=message, status_code=status_code) 16 | 17 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/managers.py: -------------------------------------------------------------------------------- 1 | from django_ripozo import DjangoManager 2 | from .models import TaskBoard, Task 3 | 4 | class TaskBoardManager(DjangoManager): 5 | # These are the default fields to use when performing any action 6 | fields = ('id', 'title', 'task_set.id',) 7 | update_fields = ('title',) # These are the only fields allowed when updating. 8 | model = TaskBoard 9 | paginate_by = 10 10 | 11 | class TaskManager(DjangoManager): 12 | fields = ('id', 'title', 'description', 'completed', 'task_board_id',) 13 | model = Task 14 | paginate_by = 20 15 | -------------------------------------------------------------------------------- /testapp/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django_ripozo.dispatcher import DjangoDispatcher 7 | 8 | from ripozo.adapters import SirenAdapter, HalAdapter 9 | 10 | from testapp.resources import HelloResource, HelloWorldIdResource 11 | 12 | 13 | dispatcher = DjangoDispatcher(base_url='/api') 14 | dispatcher.register_resources(HelloResource, HelloWorldIdResource) 15 | dispatcher.register_adapters(SirenAdapter, HalAdapter) 16 | 17 | urlpatterns = dispatcher.url_patterns 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.3" 5 | - "2.7" 6 | - "pypy" 7 | - "pypy3" 8 | env: 9 | - DJANGO_VERSION="Django<1.9" RIPOZO="-U ripozo" 10 | - DJANGO_VERSION="Django<1.8" RIPOZO="-U ripozo" 11 | - DJANGO_VERSION="Django<1.7" RIPOZO="-U ripozo" 12 | - DJANGO_VERSION="Django<1.9" RIPOZO="git+git://github.com/vertical-knowledge/ripozo.git@master" 13 | install: 14 | - pip install coveralls 15 | - pip install --pre $DJANGO_VERSION 16 | - pip install $RIPOZO 17 | - python setup.py -q install 18 | script: 19 | coverage run --source=django_ripozo setup.py test 20 | after_success: 21 | coveralls -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 1.1.1 (unreleased) 5 | ================== 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 1.1.0 (2015-11-22) 11 | ================== 12 | 13 | - Added Easy Resources which allow a user to easily create resources from django models 14 | 15 | 16 | 1.0.0 (2015-06-30) 17 | ================== 18 | 19 | - Nothing changed yet. 20 | 21 | 22 | 0.2.0 (2015-06-29) 23 | ================== 24 | 25 | - Removed support for Django 1.5 26 | - Using Accept-Types instead of Content-Type. 27 | 28 | 0.1.1 (2015-04-23) 29 | ================== 30 | 31 | - Added tests and fixes for relationships 32 | - initial release -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. django-ripozo documentation master file, created by 2 | sphinx-quickstart on Tue Jun 16 14:46:53 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to django-ripozo's documentation! 7 | ========================================= 8 | 9 | Contents: 10 | 11 | .. toctree:: 12 | :maxdepth: 2 13 | 14 | tutorial/setup.rst 15 | 16 | 17 | .. include:: ../../README.rst 18 | 19 | .. include:: ../../CHANGELOG.rst 20 | 21 | Indices and tables 22 | ================== 23 | 24 | * :ref:`genindex` 25 | * :ref:`modindex` 26 | * :ref:`search` 27 | 28 | -------------------------------------------------------------------------------- /django_ripozo_tests/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | import os 7 | 8 | __author__ = 'Tim Martin' 9 | 10 | 11 | base_dir = os.path.dirname(__file__) 12 | project = os.path.join(base_dir, '..', 'test_app') 13 | # os.environ['PATH'] += ':{0}'.format(project) 14 | # os.environ['PATH'] += ':{0}'.format(os.path.join(project, 'test_project')) 15 | # os.environ['PATH'] += ':{0}'.format(os.path.join(project, 'testapp')) 16 | os.environ['DJANGO_SETTINGS_MODULE'] = 'test_project.settings' 17 | 18 | from . import helpers, integration, unit 19 | -------------------------------------------------------------------------------- /testapp/resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.decorators import apimethod 7 | from ripozo.resources.resource_base import ResourceBase 8 | 9 | 10 | class HelloResource(ResourceBase): 11 | resource_name = 'myresource' 12 | 13 | @apimethod(methods=['GET']) 14 | def say_hello(cls, request, *args, **kwargs): 15 | return cls(properties=dict(hello='world')) 16 | 17 | 18 | class HelloWorldIdResource(ResourceBase): 19 | resource_name = 'myidresource' 20 | pks = ['id'] 21 | 22 | @apimethod(methods=['GET']) 23 | def say_hello(cls, request, *args, **kwargs): 24 | return cls(properties=dict(hello='world', id=request.url_params['id'])) 25 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/resources.py: -------------------------------------------------------------------------------- 1 | from ripozo import restmixins, ListRelationship, Relationship, apimethod 2 | from .managers import TaskBoardManager, TaskManager 3 | 4 | class TaskBoardResource(restmixins.CRUDL): 5 | manager = TaskBoardManager() 6 | resource_name = 'taskboard' 7 | pks = ('id',) 8 | _relationships = ( 9 | ListRelationship('task_set', relation='TaskResource'), 10 | ) 11 | 12 | # We're going to add a simple way to add 13 | # tasks to a board by extending the 14 | @apimethod(route='/addtask', methods=['POST']) 15 | def add_task(cls, request): 16 | body_args = request.body_args 17 | body_args['task_board_id'] = request.get('id') 18 | request.body_args = body_args 19 | return TaskResource.create(request) 20 | 21 | class TaskResource(restmixins.CRUD): 22 | manager = TaskManager() 23 | resource_name = 'task' 24 | pks = ('id',) 25 | _relationships = ( 26 | Relationship('task_board', property_map=dict(task_board_id='id'), relation='TaskBoardResource'), 27 | ) 28 | -------------------------------------------------------------------------------- /django_ripozo_tests/integration/basic.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django.test import Client 7 | 8 | from django_ripozo_tests.helpers.common import UnittestBase 9 | 10 | import json 11 | import logging 12 | import six 13 | import unittest2 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class TestBasic(UnittestBase, unittest2.TestCase): 19 | def setUp(self): 20 | self.client = Client() 21 | 22 | def test_list_url(self): 23 | response = self.client.get('/api/myresource/') 24 | self.assertEqual(response.status_code, 200) 25 | content = six.text_type(response.content, 'utf8') 26 | data = json.loads(content) 27 | self.assertDictEqual(data['properties'], dict(hello='world')) 28 | 29 | def test_individual_url(self): 30 | response = self.client.get('/api/myidresource/1/') 31 | self.assertEqual(response.status_code, 200) 32 | content = six.text_type(response.content, 'utf8') 33 | data = json.loads(content) 34 | self.assertDictEqual(data['properties'], dict(hello='world', id='1')) 35 | -------------------------------------------------------------------------------- /examples/todolist/todoapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Task', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('title', models.CharField(max_length=50)), 18 | ('description', models.TextField()), 19 | ('completed', models.BooleanField(default=False)), 20 | ], 21 | ), 22 | migrations.CreateModel( 23 | name='TaskBoard', 24 | fields=[ 25 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 26 | ('title', models.CharField(max_length=50)), 27 | ], 28 | ), 29 | migrations.AddField( 30 | model_name='task', 31 | name='task_board', 32 | field=models.ForeignKey(to='todoapp.TaskBoard'), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | .eggs/ 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # PyCharm IDE 60 | .idea/ 61 | 62 | .git.bfg-report/2015-02-05/19-57-11/cache-stats.txt 63 | 64 | .git.bfg-report/2015-02-05/19-57-11/object-id-map.old-new.txt 65 | 66 | # PyPi private file 67 | .pypirc 68 | 69 | setup.cfg 70 | 71 | # SQLite files 72 | *.sqlite3 73 | *.sqlite 74 | 75 | -------------------------------------------------------------------------------- /examples/django_example_requests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from pypermedia.client import HypermediaClient 7 | import requests 8 | 9 | 10 | if __name__ == '__main__': 11 | siren_client = HypermediaClient.connect('http://localhost:8000/api/taskboard/', request_factory=requests.Request) 12 | task_board_list = siren_client.retrieve_list() 13 | created = task_board_list.create(title='My First Board') 14 | 15 | retrieve = created.retrieve() 16 | print(created.title) 17 | print(created.id) 18 | 19 | updated = retrieve.update(title='My Updated Board') 20 | print(updated.title) 21 | 22 | new_task = updated.add_task(title='My first task', description='I need to do something') 23 | print(new_task.title) 24 | print(new_task.description) 25 | print(new_task.completed) 26 | 27 | task_board = retrieve.retrieve() 28 | 29 | task = next(task_board.get_entities('task_set')) 30 | print(task.description) 31 | print(task.completed) 32 | 33 | task = task.update(completed=True) 34 | print(task.completed) 35 | 36 | parent_board = next(task.get_entities('task_board')) 37 | print(parent_board.title) 38 | 39 | deleted = task.delete() 40 | 41 | original_task = task.retrieve() 42 | print(original_task) 43 | -------------------------------------------------------------------------------- /django_ripozo_tests/helpers/common.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django.core.management import call_command 7 | 8 | import cProfile 9 | import django 10 | import random 11 | import pstats 12 | import string 13 | 14 | 15 | def profileit(func): 16 | """ 17 | Decorator straight up stolen from stackoverflow 18 | """ 19 | def wrapper(*args, **kwargs): 20 | datafn = func.__name__ + ".profile" # Name the data file sensibly 21 | prof = cProfile.Profile() 22 | prof.enable() 23 | retval = prof.runcall(func, *args, **kwargs) 24 | prof.disable() 25 | stats = pstats.Stats(prof) 26 | try: 27 | stats.sort_stats('cumtime').print_stats(10) 28 | except KeyError: 29 | pass # breaks in python 2.6 30 | try: 31 | stats.sort_stats('tottime').print_stats(10) 32 | except KeyError: 33 | pass # breaks in python 2.6 34 | return retval 35 | 36 | return wrapper 37 | 38 | 39 | def random_string(length=50): 40 | return ''.join(random.choice(string.ascii_letters) for _ in range(length)) 41 | 42 | 43 | class UnittestBase(object): 44 | @classmethod 45 | def setUpClass(cls): 46 | try: 47 | django.setup() 48 | except AttributeError: 49 | pass 50 | call_command('syncdb', interactive=False) 51 | try: 52 | call_command('makemigrations') 53 | call_command('migrate', interactive=False) 54 | except: 55 | pass 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | # from __future__ import unicode_literals 5 | 6 | __author__ = 'Tim Martin' 7 | 8 | from setuptools import setup 9 | 10 | version = '1.1.1.dev0' 11 | 12 | setup( 13 | author=__author__, 14 | author_email='tim.martin@vertical-knowledge.com', 15 | name='django-ripozo', 16 | version=version, 17 | packages=[ 18 | 'django_ripozo', 19 | 'django_ripozo.migrations' 20 | ], 21 | description=('Integrates ripozo with django for fast, ' 22 | 'flexible Hypermedia, HATEOAS, and other REST apis'), 23 | install_requires=[ 24 | 'ripozo>=1.0.0', 25 | 'Django>=1.6', 26 | 'six' 27 | ], 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Framework :: Django :: 1.6', 31 | 'Framework :: Django :: 1.7', 32 | 'Framework :: Django :: 1.8', 33 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3', 39 | 'Programming Language :: Python :: 3.3', 40 | 'Programming Language :: Python :: 3.4', 41 | 'Programming Language :: Python :: Implementation :: PyPy', 42 | 'Topic :: Software Development :: Libraries :: Python Modules', 43 | ], 44 | tests_require=[ 45 | 'mock', 46 | 'unittest2' 47 | ], 48 | test_suite='django_ripozo_tests', 49 | url='http://django-ripozo.readthedocs.org/' 50 | ) 51 | -------------------------------------------------------------------------------- /testapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class MyModel(models.Model): 5 | """ 6 | Doesn't include relationships or files for now 7 | """ 8 | biginteger = models.BigIntegerField() 9 | boolean = models.BooleanField(default=False) 10 | char = models.CharField(max_length=100) 11 | csi = models.CommaSeparatedIntegerField(max_length=100) 12 | date_a = models.DateField() 13 | datetime_a = models.DateTimeField() 14 | decimal_a = models.DecimalField(max_digits=5, decimal_places=2) 15 | email = models.EmailField() 16 | float_a = models.FloatField() 17 | integer = models.IntegerField() 18 | ipaddress = models.IPAddressField() 19 | genericip = models.GenericIPAddressField() 20 | nullbool = models.NullBooleanField() 21 | positiveint = models.PositiveIntegerField() 22 | positivesmallint = models.PositiveSmallIntegerField() 23 | slug = models.SlugField() 24 | smallint = models.SmallIntegerField() 25 | time_a = models.TimeField() 26 | url = models.URLField() 27 | 28 | 29 | class OneToMany(models.Model): 30 | """ 31 | This one model has many ManyToOne models. 32 | """ 33 | one_value = models.CharField(max_length=63) 34 | 35 | 36 | class ManyToOne(models.Model): 37 | """ 38 | Many of this model have one OneToMany models 39 | """ 40 | one = models.ForeignKey('OneToMany', related_name='manies') 41 | many_value = models.CharField(max_length=63) 42 | 43 | 44 | class ManyToManyFirst(models.Model): 45 | value = models.CharField(max_length=63) 46 | 47 | 48 | class ManyToManySecond(models.Model): 49 | value = models.CharField(max_length=63) 50 | many_to_many = models.ManyToManyField(ManyToManyFirst, related_name='all_the_manies') 51 | 52 | 53 | class OneFirst(models.Model): 54 | value = models.CharField(max_length=63) 55 | 56 | 57 | class OneSecond(models.Model): 58 | value = models.CharField(max_length=63) 59 | first = models.OneToOneField(OneFirst, related_name='second') 60 | -------------------------------------------------------------------------------- /django_ripozo_tests/integration/dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django_ripozo.dispatcher import DjangoDispatcher, MethodRouter 7 | 8 | from django_ripozo_tests.helpers.common import UnittestBase 9 | 10 | import unittest2 11 | 12 | 13 | class TestDispatcher(UnittestBase, unittest2.TestCase): 14 | def test_register_route(self): 15 | """ 16 | Tests the register_route method on the DjangoDispatcher instance 17 | """ 18 | d = DjangoDispatcher() 19 | def fake(): 20 | pass 21 | d.register_route('fake', endpoint_func=fake, route='route', methods=['GET']) 22 | route = DjangoDispatcher._convert_url_to_regex('route') 23 | self.assertIn(route, d.url_map) 24 | old_router = d.url_map[route] 25 | self.assertIsInstance(d.url_map[route], MethodRouter) 26 | def fake2(): 27 | pass 28 | d.register_route('fake2', endpoint_func=fake2, route='route', methods=['POST']) 29 | self.assertEqual(old_router, d.url_map[route]) 30 | self.assertEqual(len(old_router.method_map), 2) 31 | 32 | def test_url_patterns_property(self): 33 | """ 34 | Test to ensure that the url_patterns propery 35 | appropriately gets the django patterns object 36 | """ 37 | d = DjangoDispatcher() 38 | self.assertIsInstance(d.url_patterns, list) 39 | self.assertEqual(len(d.url_patterns), 1) 40 | 41 | def fake(): 42 | pass 43 | d.register_route('fake', endpoint_func=fake, route='route', methods=['GET']) 44 | route = DjangoDispatcher._convert_url_to_regex('route') 45 | self.assertIsInstance(d.url_patterns, list) 46 | regexes = [p._regex for p in d.url_patterns] 47 | self.assertEqual(len(regexes), 2) 48 | self.assertIn(route, regexes) 49 | self.assertIn('^$', regexes) 50 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for test_app project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'sca*+)zkv$tlrdu@aj8d-cvl6-ys#^)n@+y@1&l%i)xhx*boej' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django_ripozo', 35 | 'testapp', 36 | ) 37 | 38 | MIDDLEWARE_CLASSES = tuple() 39 | 40 | ROOT_URLCONF = 'testapp.urls' 41 | 42 | TEMPLATES = [ 43 | { 44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 45 | 'DIRS': [], 46 | 'APP_DIRS': True, 47 | 'OPTIONS': { 48 | 'context_processors': [ 49 | 'django.template.context_processors.debug', 50 | 'django.template.context_processors.request', 51 | 'django.contrib.auth.context_processors.auth', 52 | 'django.contrib.messages.context_processors.messages', 53 | ], 54 | }, 55 | }, 56 | ] 57 | 58 | WSGI_APPLICATION = 'test_app.wsgi.application' 59 | 60 | 61 | # Database 62 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 63 | 64 | DATABASES = { 65 | 'default': { 66 | 'ENGINE': 'django.db.backends.sqlite3', 67 | 'NAME': ':memory:' 68 | } 69 | } 70 | 71 | 72 | # Internationalization 73 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 74 | 75 | LANGUAGE_CODE = 'en-us' 76 | 77 | TIME_ZONE = 'UTC' 78 | 79 | USE_I18N = True 80 | 81 | USE_L10N = True 82 | 83 | USE_TZ = False 84 | 85 | 86 | # Static files (CSS, JavaScript, Images) 87 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 88 | 89 | STATIC_URL = '/static/' 90 | -------------------------------------------------------------------------------- /django_ripozo_tests/unit/dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django_ripozo import DjangoDispatcher 7 | 8 | import mock 9 | import re 10 | import unittest2 11 | 12 | 13 | class TestDjangoDispatcher(unittest2.TestCase): 14 | def test_url_map(self): 15 | d = DjangoDispatcher() 16 | self.assertEqual(len(d.url_map), 1) 17 | self.assertIn('^$', d.url_map) 18 | 19 | def test_base_url(self): 20 | d = DjangoDispatcher(base_url='blah') 21 | self.assertEqual(d.base_url, 'blah') 22 | 23 | def test_url_patterns(self): 24 | """ 25 | Test to ensure that the url_patterns propery 26 | appropriately gets the django patterns object 27 | """ 28 | d = DjangoDispatcher() 29 | self.assertIsInstance(d.url_patterns, list) 30 | self.assertEqual(len(d.url_patterns), 1) 31 | 32 | def fake(): 33 | pass 34 | d.register_route('fake', endpoint_func=fake, route='route', methods=['GET']) 35 | route = DjangoDispatcher._convert_url_to_regex('route') 36 | self.assertIsInstance(d.url_patterns, list) 37 | regexes = [p._regex for p in d.url_patterns] 38 | self.assertEqual(len(regexes), 2) 39 | self.assertIn(route, regexes) 40 | self.assertIn('^$', regexes) 41 | 42 | def test_register_route(self): 43 | d = DjangoDispatcher(method_route_class=mock.MagicMock()) 44 | d.register_route('fake', route='fake2') 45 | mr = d.url_map['^fake2$'] 46 | self.assertIsInstance(mr, mock.MagicMock) 47 | self.assertEqual(mr.add_route.call_count, 2) 48 | 49 | def test_convert_url_to_regex(self): 50 | """ 51 | Tests the ``DjangoDispatcher_convert_url_to_regex`` static method 52 | """ 53 | urls = [('/api/resource', '^api/resource$'), 54 | ('', '^$'), 55 | ('/api/resource/', '^api/resource/(?P[^/]+)$'), 56 | ('/api/resource//', '^api/resource/(?P[^/]+)/(?P[^/]+)$'), 57 | ('/api/resource//', '^api/resource/(?P[^/]+)/(?P[^/]+)$'), 58 | ('/api/resource//another', '^api/resource/(?P[^/]+)/another$')] 59 | for input_url, output in urls: 60 | self.assertEqual(DjangoDispatcher._convert_url_to_regex(input_url), output) 61 | -------------------------------------------------------------------------------- /testapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='ManyToOne', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('many_value', models.CharField(max_length=63)), 18 | ], 19 | ), 20 | migrations.CreateModel( 21 | name='MyModel', 22 | fields=[ 23 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 24 | ('biginteger', models.BigIntegerField()), 25 | ('boolean', models.BooleanField(default=False)), 26 | ('char', models.CharField(max_length=100)), 27 | ('csi', models.CommaSeparatedIntegerField(max_length=100)), 28 | ('date_a', models.DateField()), 29 | ('datetime_a', models.DateTimeField()), 30 | ('decimal_a', models.DecimalField(max_digits=5, decimal_places=2)), 31 | ('email', models.EmailField(max_length=254)), 32 | ('float_a', models.FloatField()), 33 | ('integer', models.IntegerField()), 34 | ('ipaddress', models.IPAddressField()), 35 | ('genericip', models.GenericIPAddressField()), 36 | ('nullbool', models.NullBooleanField()), 37 | ('positiveint', models.PositiveIntegerField()), 38 | ('positivesmallint', models.PositiveSmallIntegerField()), 39 | ('slug', models.SlugField()), 40 | ('smallint', models.SmallIntegerField()), 41 | ('time_a', models.TimeField()), 42 | ('url', models.URLField()), 43 | ], 44 | ), 45 | migrations.CreateModel( 46 | name='OneToMany', 47 | fields=[ 48 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 49 | ('one_value', models.CharField(max_length=63)), 50 | ], 51 | ), 52 | migrations.AddField( 53 | model_name='manytoone', 54 | name='one', 55 | field=models.ForeignKey(related_name='manies', to='testapp.OneToMany'), 56 | ), 57 | ] 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-ripozo 2 | ============= 3 | 4 | .. image:: https://travis-ci.org/vertical-knowledge/django-ripozo.svg?branch=master&style=flat 5 | :target: https://travis-ci.org/vertical-knowledge/django-ripozo 6 | :alt: test status 7 | 8 | .. image:: https://coveralls.io/repos/vertical-knowledge/django-ripozo/badge.svg?branch=master&style=flat 9 | :target: https://coveralls.io/r/vertical-knowledge/django-ripozo?branch=master 10 | :alt: test coverage 11 | 12 | .. image:: https://readthedocs.org/projects/django-ripozo/badge/?version=latest&style=flat 13 | :target: https://django-ripozo.readthedocs.org/ 14 | :alt: documentation status 15 | 16 | Integrates ripozo with django for fast, flexible 17 | Hypermedia, HATEOAS, and other REST apis. 18 | 19 | `Full django-ripozo documentation `_ 20 | 21 | Looking for the `ripozo documentation? `_ 22 | 23 | Supports Django 1.6, 1.7, and 1.8. 24 | 25 | python 2.6, 2.7, 3.3, 3.4, pypy 26 | 27 | NOTE 28 | ---- 29 | 30 | Currently there are `compatibility issues `_ 31 | with Django 1.6, 1.7 and python 3.5. There is currently a fix in progress. 32 | However, two of our test environments will fail until the fix is deployed. All tests 33 | pass otherwise. 34 | 35 | Minimal App 36 | ----------- 37 | 38 | You'll need to instantiate the django project in 39 | the standard manner. If you aren't sure how to 40 | do this, check out the excellent 41 | `django documentation. `_ 42 | 43 | In your app you'll need a resources.py file. 44 | 45 | .. code-block:: python 46 | 47 | from ripozo import ResourceBase, apimethod 48 | 49 | class MyResource(ResourceBase): 50 | @apimethod(methods=['GET']) 51 | def say_hello(cls, request): 52 | return cls(properties=dict(hello='world')) 53 | 54 | And in your urls.py file 55 | 56 | .. code-block:: python 57 | 58 | from ripozo.adapters import SirenAdapter, HalAdapter 59 | from .resources import MyResource 60 | 61 | dispatcher = DjangoDispatcher() 62 | dispatcher.register_resources(MyResource) 63 | dispatcher.register_adapters(SirenAdapter, HalAdapter) 64 | 65 | urlpatterns = dispatcher.url_patterns 66 | 67 | And just like that you have a fully functional application. 68 | 69 | Looking for a more extensive example? 70 | Check out an `example `_ 71 | with database interactions as well. 72 | -------------------------------------------------------------------------------- /django_ripozo_tests/unit/onetomanymanager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django_ripozo.manager import DjangoManager 7 | 8 | from django_ripozo_tests.helpers.common import UnittestBase, random_string 9 | 10 | from testapp.models import OneToMany, ManyToOne 11 | 12 | import unittest2 13 | 14 | 15 | class TestDjangoManager(UnittestBase, unittest2.TestCase): 16 | def setUp(self): 17 | class OneManager(DjangoManager): 18 | model = OneToMany 19 | fields = ['id', 'one_value', 'manies.id', 'manies.many_value'] 20 | 21 | class ManyManager(DjangoManager): 22 | # TODO Test one creation 23 | model = ManyToOne 24 | fields = ['id', 'many_value', 'one_id'] 25 | 26 | self.one = OneManager() 27 | self.many = ManyManager() 28 | 29 | def create_one(self, one_value=None): 30 | one_value = one_value or random_string() 31 | return OneToMany.objects.create(one_value=one_value) 32 | 33 | def create_many(self, many_value=None, one=None): 34 | one = one or self.create_one() 35 | many_value = many_value or random_string() 36 | return ManyToOne.objects.create(many_value=many_value, one=one) 37 | 38 | def test_create_one(self): 39 | val = random_string() 40 | resp = self.one.create(dict(one_value=val)) 41 | id_ = resp.get('id') 42 | model = OneToMany.objects.get(id=id_) 43 | self.assertEqual(val, model.one_value) 44 | 45 | def test_create_many(self): 46 | one = self.create_one() 47 | resp = self.many.create(dict(many_value='something', one_id=one.id)) 48 | many = ManyToOne.objects.get(id=resp.get('id')) 49 | self.assertIn(many, one.manies.all()) 50 | 51 | def test_retrieve_one(self): 52 | one = self.create_one() 53 | many1 = self.create_many(one=one) 54 | many2 = self.create_many(one=one) 55 | resp = self.one.retrieve(dict(id=one.id)) 56 | self.assertEqual(one.one_value, resp['one_value']) 57 | self.assertEqual(one.id, resp['id']) 58 | self.assertEqual(len(resp['manies']), 2) 59 | self.assertIsInstance(resp['manies'], list) 60 | found1 = found2 = False 61 | for m in resp['manies']: 62 | if m['id'] == many1.id: 63 | self.assertEqual(m['many_value'], many1.many_value) 64 | found1 = True 65 | elif m['id'] == many2.id: 66 | self.assertEqual(m['many_value'], many2.many_value) 67 | found2 = True 68 | self.assertTrue(found1 == found2 == True) 69 | 70 | -------------------------------------------------------------------------------- /examples/todolist/todolist/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for todolist project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'eof+114uu36-m=38j*6hwv$%#)y&!kejojnrg#*up20^zwkej_' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'django_ripozo', 41 | 'todoapp', 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | 'django.middleware.security.SecurityMiddleware', 53 | ) 54 | 55 | ROOT_URLCONF = 'todolist.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'todolist.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Internationalization 88 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 89 | 90 | LANGUAGE_CODE = 'en-us' 91 | 92 | TIME_ZONE = 'UTC' 93 | 94 | USE_I18N = True 95 | 96 | USE_L10N = True 97 | 98 | USE_TZ = True 99 | 100 | 101 | # Static files (CSS, JavaScript, Images) 102 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 103 | 104 | STATIC_URL = '/static/' 105 | -------------------------------------------------------------------------------- /django_ripozo_tests/integration/easy_resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from ripozo.resources.relationships import Relationship, ListRelationship 7 | 8 | from django_ripozo.easy_resources import _get_pks, _get_fields_for_model, \ 9 | _get_relationships, create_resource 10 | import unittest2 11 | 12 | from testapp.models import OneToMany, ManyToOne, MyModel, ManyToManyFirst, \ 13 | ManyToManySecond, OneFirst, OneSecond 14 | 15 | 16 | class TestEasyResource(unittest2.TestCase): 17 | def test_get_pks(self): 18 | resp = _get_pks(MyModel) 19 | self.assertTupleEqual(('id',), resp) 20 | 21 | def test_get_fields(self): 22 | resp = _get_fields_for_model(OneToMany) 23 | self.assertEqual(set(resp), set(['id', 'one_value', 'manies.id'])) 24 | 25 | def test_get_relationships_one_to_many(self): 26 | resp = _get_relationships(OneToMany) 27 | self.assertEqual(len(resp), 1) 28 | self.assertIsInstance(resp, tuple) 29 | rel = resp[0] 30 | self.assertEqual(rel.name, 'manies') 31 | self.assertEqual(rel._relation, 'ManyToOne') 32 | self.assertIsInstance(rel, ListRelationship) 33 | 34 | def test_get_relationships_many_to_one(self): 35 | resp = _get_relationships(ManyToOne) 36 | self.assertEqual(len(resp), 1) 37 | self.assertIsInstance(resp, tuple) 38 | rel = resp[0] 39 | self.assertEqual(rel.name, 'one') 40 | self.assertEqual(rel._relation, 'OneToMany') 41 | self.assertIsInstance(rel, Relationship) 42 | 43 | def test_create_resource(self): 44 | res = create_resource(OneToMany) 45 | self.assertEqual(len(res._relationships), 1) 46 | self.assertEqual(res.resource_name, 'one_to_many') 47 | self.assertTupleEqual(res.pks, ('id',)) 48 | res2 = create_resource(ManyToOne) 49 | self.assertEqual(len(res2._relationships), 1) 50 | self.assertEqual(res2.resource_name, 'many_to_one') 51 | self.assertTupleEqual(res2.pks, ('id',)) 52 | 53 | def test_get_relationships_many_to_many_implicit(self): 54 | resp = _get_relationships(ManyToManyFirst) 55 | self.assertEqual(len(resp), 1) 56 | self.assertIsInstance(resp, tuple) 57 | rel = resp[0] 58 | self.assertEqual(rel.name, 'all_the_manies') 59 | self.assertEqual(rel._relation, 'ManyToManySecond') 60 | self.assertIsInstance(rel, ListRelationship) 61 | 62 | def test_get_relationships_many_to_many_explicit(self): 63 | resp = _get_relationships(ManyToManySecond) 64 | self.assertEqual(len(resp), 1) 65 | self.assertIsInstance(resp, tuple) 66 | rel = resp[0] 67 | self.assertEqual(rel.name, 'many_to_many') 68 | self.assertEqual(rel._relation, 'ManyToManyFirst') 69 | self.assertIsInstance(rel, ListRelationship) 70 | 71 | def test_get_relationships_one_to_one_implicit(self): 72 | resp = _get_relationships(OneFirst) 73 | self.assertEqual(len(resp), 1) 74 | self.assertIsInstance(resp, tuple) 75 | rel = resp[0] 76 | self.assertEqual(rel.name, 'second') 77 | self.assertEqual(rel._relation, 'OneSecond') 78 | self.assertIsInstance(rel, Relationship) 79 | 80 | def test_get_relationships_one_to_one_explicit(self): 81 | resp = _get_relationships(OneSecond) 82 | self.assertEqual(len(resp), 1) 83 | self.assertIsInstance(resp, tuple) 84 | rel = resp[0] 85 | self.assertEqual(rel.name, 'first') 86 | self.assertEqual(rel._relation, 'OneFirst') 87 | self.assertIsInstance(rel, Relationship) 88 | 89 | -------------------------------------------------------------------------------- /django_ripozo_tests/integration/method_router.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django.http import HttpResponseNotAllowed, HttpResponse 7 | 8 | from django_ripozo.dispatcher import MethodRouter 9 | from django_ripozo.exceptions import MethodNotAllowed 10 | 11 | from django_ripozo_tests.helpers.common import UnittestBase 12 | 13 | from ripozo.exceptions import RestException 14 | 15 | import mock 16 | import unittest2 17 | 18 | 19 | class TestDispatcher(UnittestBase, unittest2.TestCase): 20 | """ 21 | unit tests for the ``MethodRouter`` class 22 | """ 23 | 24 | def test_add_route(self): 25 | """ 26 | Ensure that the add_route method works as intended 27 | """ 28 | mr = MethodRouter(None, None) 29 | self.assertEqual(mr.method_map, {}) 30 | def fake(): 31 | pass 32 | mr.add_route(endpoint_func=fake, methods=['GET']) 33 | self.assertEqual(mr.method_map['get'], fake) 34 | self.assertRaises(ValueError, mr.add_route, endpoint_func=fake, methods=['GET']) 35 | self.assertRaises(ValueError, mr.add_route, endpoint_func=fake, methods=['get']) 36 | 37 | mr = MethodRouter(None, None) 38 | self.assertRaises(ValueError, mr.add_route, endpoint_func=fake, methods=['GET', 'get']) 39 | 40 | mr = MethodRouter(None, None) 41 | mr.add_route(endpoint_func=fake, methods=['POST', 'GET']) 42 | self.assertEqual(mr.method_map['get'], fake) 43 | self.assertEqual(mr.method_map['post'], fake) 44 | 45 | mr.add_route(endpoint_func=fake, methods=['PATCH', 'put']) 46 | self.assertEqual(mr.method_map['patch'], fake) 47 | self.assertEqual(mr.method_map['put'], fake) 48 | 49 | def test_call(self): 50 | """ 51 | Tests whether calling an instance of MethodRouter 52 | properly dispatches an apimethod 53 | """ 54 | dispatcher = mock.MagicMock() 55 | adapter = mock.MagicMock() 56 | adapter.formatted_body = 'some_body' 57 | adapter.extra_headers = {'some': 'header'} 58 | dispatcher.dispatch.return_value = adapter 59 | mr = MethodRouter(None, dispatcher) 60 | 61 | def fake(): 62 | pass 63 | mr.add_route(endpoint_func=fake, methods=['GET', 'post']) 64 | 65 | request = mock.MagicMock() 66 | 67 | request.method = 'get' 68 | resp = mr(request) 69 | self.assertIsInstance(resp, HttpResponse) 70 | self.assertEqual(dispatcher.dispatch.call_count, 1) 71 | 72 | request.method = 'post' 73 | resp = mr(request) 74 | self.assertIsInstance(resp, HttpResponse) 75 | self.assertIsInstance(resp, HttpResponse) 76 | self.assertEqual(dispatcher.dispatch.call_count, 2) 77 | 78 | def test_get_func_for_method(self): 79 | """ 80 | tests the get_func_for_method 81 | """ 82 | mr = MethodRouter(None, None) 83 | 84 | def fake(): 85 | pass 86 | mr.add_route(endpoint_func=fake, methods=['GET', 'post']) 87 | 88 | def fake2(): 89 | pass 90 | mr.add_route(endpoint_func=fake2, methods=['PATCH']) 91 | 92 | self.assertEqual(mr.get_func_for_method('GET'), fake) 93 | self.assertEqual(mr.get_func_for_method('get'), fake) 94 | 95 | self.assertEqual(mr.get_func_for_method('POST'), fake) 96 | self.assertEqual(mr.get_func_for_method('post'), fake) 97 | 98 | self.assertEqual(mr.get_func_for_method('PATCH'), fake2) 99 | self.assertEqual(mr.get_func_for_method('patch'), fake2) 100 | 101 | self.assertRaises(MethodNotAllowed, mr.get_func_for_method, 'PUT') 102 | self.assertRaises(MethodNotAllowed, mr.get_func_for_method, 'put') 103 | -------------------------------------------------------------------------------- /django_ripozo_tests/unit/method_router.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django.http import HttpResponseNotAllowed, HttpResponse 7 | 8 | from django_ripozo.dispatcher import MethodRouter, default_error_handler 9 | from django_ripozo.exceptions import MethodNotAllowed 10 | 11 | from django_ripozo_tests.helpers.common import UnittestBase 12 | 13 | from ripozo.adapters import SirenAdapter 14 | from ripozo.exceptions import RestException 15 | 16 | import django 17 | import mock 18 | import six 19 | import unittest2 20 | 21 | 22 | class TestMethodRouter(unittest2.TestCase): 23 | @classmethod 24 | def setUpClass(cls): 25 | try: 26 | django.setup() 27 | except AttributeError: 28 | pass 29 | 30 | def test_default_error_handler(self): 31 | """ 32 | Tests in the case of a RestException 33 | """ 34 | re = RestException('blah', status_code=499) 35 | resp = default_error_handler(None, None, SirenAdapter, re) 36 | self.assertIsInstance(resp, HttpResponse) 37 | self.assertEqual(resp.status_code, 499) 38 | 39 | def test_default_error_handler_non_ripozo(self): 40 | """ 41 | Tests in the case of an unknown exception. 42 | """ 43 | e = ValueError('blah') 44 | self.assertRaises(ValueError, default_error_handler, None, None, None, e) 45 | 46 | def test_add_route_casing(self): 47 | """ 48 | Tests that the appropriate case (lower) 49 | case is always implicitly forced. 50 | """ 51 | mr = MethodRouter(None, None) 52 | methods = ('FIRST', 'Second', 'thIrd', 'fourth') 53 | mr.add_route(endpoint_func=1, methods=methods) 54 | for m in mr.method_map.keys(): 55 | self.assertTrue(m.islower()) 56 | 57 | def test_add_route_value_error(self): 58 | """ 59 | Tests that a value error is raised if 60 | the same method is already in the method router. 61 | """ 62 | mr = MethodRouter(None, None) 63 | methods = ('FIRST', 'first') 64 | self.assertRaises(ValueError, mr.add_route, endpoint_func=1, methods=methods) 65 | 66 | def test_method_map_property(self): 67 | mr = MethodRouter(None, None) 68 | self.assertDictEqual(mr.method_map, {}) 69 | 70 | def test_get_func_for_method_available(self): 71 | """ 72 | Tests that the get_func_for_method works appropriately 73 | """ 74 | mr = MethodRouter(None, None) 75 | mr.add_route(endpoint_func=1, methods=['GET']) 76 | self.assertEqual(mr.get_func_for_method('GET'), 1) 77 | 78 | def test_get_func_for_method_unavailable(self): 79 | """ 80 | Tests that the get_func_for_method works appropriately 81 | """ 82 | mr = MethodRouter(None, None) 83 | self.assertRaises(MethodNotAllowed, mr.get_func_for_method, 'GET') 84 | 85 | def test_call_normal_case(self): 86 | """ 87 | Tests calling the method router under normal case. 88 | """ 89 | dispatcher = mock.MagicMock() 90 | adapter = mock.Mock(formatted_body='some_body', extra_headers={'some': 'header'}, status_code=42) 91 | dispatcher.dispatch.return_value = adapter 92 | mr = MethodRouter(None, dispatcher) 93 | mr.add_route(endpoint_func=mock.MagicMock(), methods=['GET', 'post']) 94 | request = mock.MagicMock(method='get') 95 | resp = mr(request) 96 | self.assertIsInstance(resp, HttpResponse) 97 | self.assertEqual(resp.content.decode(), 'some_body') 98 | self.assertEqual(resp.status_code, 42) 99 | 100 | def test_call_exception_handler(self): 101 | dispatcher = mock.MagicMock() 102 | adapter = mock.Mock(formatted_body='some_body', extra_headers={'some': 'header'}, status_code=42) 103 | dispatcher.dispatch.return_value = adapter 104 | error_handler = mock.MagicMock() 105 | mr = MethodRouter(None, dispatcher, error_handler=error_handler) 106 | mr.add_route(endpoint_func=mock.MagicMock(), methods=['GET', 'post']) 107 | request = mock.MagicMock(method='put') 108 | resp = mr(request) 109 | self.assertEqual(error_handler.call_count, 1) 110 | -------------------------------------------------------------------------------- /docs/source/tutorial/setup.rst: -------------------------------------------------------------------------------- 1 | django-ripozo tutorial 2 | ====================== 3 | 4 | In this tutorial we will create a todo list 5 | application. 6 | 7 | Setting up Django 8 | ----------------- 9 | 10 | *Note* This is not a tutorial for Django. If you 11 | need some more info on Django please visit: 12 | `the django tutorial `_. 13 | 14 | In this tutorial we will create a todo list 15 | application. First we need to install the 16 | necessary dependencies. It is recommended 17 | that you work within a virtualenv. 18 | 19 | .. code-block:: bash 20 | 21 | pip install django-ripozo 22 | 23 | This should install an appropriate version of Django 24 | as well. 25 | 26 | Now we need to create our project and app. 27 | 28 | .. code-block:: bash 29 | 30 | django-admin startproject todolist 31 | 32 | You'll need to sync your database. 33 | 34 | .. code-block:: bash 35 | 36 | python ./manage.py migrate 37 | 38 | Now we need to create our todo app. 39 | 40 | .. code-block:: bash 41 | 42 | python ./manage.py startapp todoapp 43 | 44 | Now in our ``todolist/settings.py`` we will want to 45 | add ``'django_ripozo'`` and ``'todoapp'`` to the 46 | INSTALLED_APPS tuple. 47 | 48 | Alright, now we're going to edit the ``todoapp/models.py`` 49 | file and add the following models. 50 | 51 | .. code-block:: python 52 | 53 | class TaskBoard(models.Model): 54 | title = models.CharField(max_length=50) 55 | 56 | 57 | class Task(models.Model): 58 | title = models.CharField(max_length=50) 59 | description = models.TextField() 60 | completed = models.BooleanField(default=False) 61 | task_board = models.ForeignKey('TaskBoard', related_name='task_set') 62 | 63 | You'll need to run makemigrations and migrate again. 64 | 65 | You're now all set up and ready to start building your api. 66 | 67 | Creating your managers 68 | ---------------------- 69 | 70 | Managers are responsible for handling access 71 | to the django models that you just created. 72 | To start we are going to create a new file in 73 | the todoapp called ``managers.py``. After that 74 | we are going to set up our managers. 75 | 76 | .. code-block:: python 77 | 78 | from django_ripozo import DjangoManager 79 | from .models import TaskBoard, Task 80 | 81 | class TaskBoardManager(DjangoManager): 82 | # These are the default fields to use when performing any action 83 | fields = ('id', 'title', 'task_set.id',) 84 | update_fields = ('title',) # These are the only fields allowed when updating. 85 | model = TaskBoard 86 | paginate_by = 10 87 | 88 | class TaskManager(DjangoManager): 89 | fields = ('id', 'title', 'description', 'completed', 'task_board_id',) 90 | model = Task 91 | paginate_by = 20 92 | 93 | That's how simple it is to set up our managers. 94 | 95 | Creating your resources 96 | ----------------------- 97 | 98 | Resources are the core of ripozo. These are common 99 | across all manager and dispatcher packages. This means, 100 | assuming that the application was developed well, you could 101 | reuse the resources in flask or mix them in with the sqlalchemy 102 | manager. 103 | 104 | The first thing we are going to do is create a file in the todoapp 105 | directory called ``resources.py``. Then we will add the following 106 | 107 | .. code-block:: python 108 | 109 | from ripozo import restmixins, ListRelationship, Relationship, apimethod 110 | from .managers import TaskBoardManager, TaskManager 111 | 112 | class TaskBoardResource(restmixins.CRUDL): 113 | manager = TaskBoardManager() 114 | resource_name = 'taskboard' 115 | pks = ('id',) 116 | _relationships = ( 117 | ListRelationship('task_set', relation='TaskResource'), 118 | ) 119 | 120 | # We're going to add a simple way to add 121 | # tasks to a board by extending the 122 | @apimethod(route='/addtask', methods=['POST']) 123 | def add_task(cls, request): 124 | body_args = request.body_args 125 | body_args['task_board_id'] = request.get('id') 126 | request.body_args = body_args 127 | return TaskResource.create(request) 128 | 129 | class TaskResource(restmixins.CRUD): 130 | manager = TaskManager() 131 | resource_name = 'task' 132 | pks = ('id',) 133 | _relationships = ( 134 | Relationship('task_board', property_map=dict(task_board_id='id'), relation='TaskBoardResource'), 135 | ) 136 | 137 | 138 | 139 | We now have a reusable core to our RESTful API. This is reusable across 140 | various web frameworks, databases (you will have to change the manager), 141 | or REST protocol. 142 | 143 | 144 | Setting up your dispatcher. 145 | --------------------------- 146 | 147 | The dispatcher is responsible for translating the 148 | request into something that the framework (Django) 149 | can understand and translating the ripozo response 150 | into the frameworks preferred method. First create a ``urls.py`` file 151 | in your todoapp directory. In that file: 152 | 153 | .. code-block:: python 154 | 155 | from django_ripozo import DjangoDispatcher 156 | from ripozo.adapters import SirenAdapter, HalAdapter 157 | from .resources import TaskBoardResource, TaskResource 158 | 159 | dispatcher = DjangoDispatcher(base_url='/api') 160 | dispatcher.register_resources(TaskBoardResource, TaskResource) 161 | dispatcher.register_adapters(SirenAdapter, HalAdapter) 162 | 163 | urlpatterns = dispatcher.url_patterns 164 | 165 | And right there you've set up your url patterns 166 | and registered the resources with the application. 167 | 168 | 169 | Using the api 170 | ============= 171 | 172 | We'll be using pypermedia to access the 173 | api. It makes it extremely easy to use 174 | a SIREN based protocol. You could use 175 | HAL protocol if you preferred by prepending 176 | that to your accept-types. 177 | 178 | .. code-block:: bash 179 | 180 | pip install pypermedia 181 | 182 | First we'll create a task board 183 | 184 | .. code-block:: python 185 | 186 | >>> siren_client = HypermediaClient.connect('http://127.0.0.1:5000/api/taskboard/', request_factory=requests.Request) 187 | >>> task_board_list = siren_client.retrieve_list() 188 | >>> created = task_board_list.create(title='My First Board') 189 | >>> retrieve = created.retrieve() 190 | >>> print(created.title) 191 | 'My First Board' 192 | >>> print(created.id) 193 | 5 194 | 195 | Now we can update the board's title. 196 | 197 | .. code-block:: python 198 | 199 | >>> updated = retrieve.update(title='My Updated Board') 200 | >>> print(updated.title) 201 | 'My Updated Board' 202 | 203 | Of course we can't have a task board without any tasks! 204 | 205 | .. code-block:: python 206 | 207 | >>> new_task = updated.add_task(title='My first task', description='I need to do something') 208 | >>> print(new_task.title) 209 | 'My first task' 210 | >>> print(new_task.description) 211 | 'I need to do something' 212 | >>> print(new_task.completed) 213 | False 214 | 215 | We can now get this task from the task board itself. 216 | 217 | .. code-block:: python 218 | 219 | >>> task_board = retrieve.retrieve() 220 | >>> task = next(task_board.get_entities('tasks')) 221 | >>> print(task.description) 222 | 'I need to do something' 223 | >>> print(task.completed) 224 | False 225 | 226 | Well I guess we did something. We'll update the task. 227 | 228 | .. code-block:: python 229 | 230 | >>> task = task.update(completed=True) 231 | >>> print(task.completed) 232 | True 233 | 234 | And we can get the board this task belonds to by getting the task_board entity 235 | 236 | .. code-block:: python 237 | 238 | >>> parent_board = next(task.get_entities('task_board')) 239 | >>> print(parent_board.title) 240 | 'My Updated Board' 241 | 242 | That task is dumb. Let's delete it. 243 | 244 | .. code-block:: python 245 | 246 | >>> deleted = task.delete() 247 | >>> original_task = task.retrieve() 248 | >>> print(original_task) 249 | None 250 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-ripozo.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-ripozo.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/django-ripozo" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-ripozo" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /django_ripozo/easy_resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | try: 7 | from django.db.models.fields.related import ForeignRelatedObjectsDescriptor, \ 8 | RenameRelatedObjectDescriptorMethods, ReverseManyRelatedObjectsDescriptor, \ 9 | ManyRelatedObjectsDescriptor, SingleRelatedObjectDescriptor 10 | except ImportError: 11 | pass # These are only for django versions < 1.8 12 | from ripozo.resources.relationships import ListRelationship, Relationship 13 | from ripozo.resources.restmixins import CRUDL 14 | from ripozo.resources.constructor import ResourceMetaClass 15 | 16 | 17 | from django_ripozo.manager import DjangoManager 18 | 19 | 20 | def _get_pks(model): 21 | """ 22 | Gets the primary key name as a tuple 23 | for a model 24 | """ 25 | return model._meta.pk.name, 26 | 27 | 28 | def _get_fields_for_model(model): 29 | """ 30 | Gets all of the fields for the model 31 | """ 32 | fields = [] 33 | try: 34 | all_fields = model._meta.get_fields() 35 | except AttributeError: # Django < 1.8 36 | return _get_fields_old_django(model) 37 | for field in all_fields: 38 | if not field.is_relation: 39 | fields.append(field.name) 40 | continue 41 | partial = field.name 42 | complete = '{0}.{1}'.format(partial, field.related_model._meta.pk.name) 43 | fields.append(complete) 44 | return fields 45 | 46 | 47 | def _get_fields_old_django(model): 48 | """ 49 | For django < 1.8 50 | """ 51 | fields = [f.name for f in model._meta.fields] 52 | for name in dir(model): 53 | attr = getattr(model, name) 54 | if isinstance(attr, ForeignRelatedObjectsDescriptor): 55 | pk = attr.related.model._meta.pk.name 56 | fields.append('{0}.{1}'.format(name, pk)) 57 | return fields 58 | 59 | 60 | def _get_relationships(model): 61 | """ 62 | Gets a tuple of appropriately constructed 63 | Relationship/ListRelationship models for the model 64 | """ 65 | relationships = [] 66 | try: 67 | all_fields = model._meta.get_fields() 68 | except AttributeError: # Django < 1.8 69 | return _get_relationships_old_django(model) 70 | for field in all_fields: 71 | if not field.is_relation: 72 | continue 73 | rel_class = ListRelationship if field.one_to_many or field.many_to_many else Relationship 74 | rel = rel_class(field.name, relation=field.related_model.__name__) 75 | relationships.append(rel) 76 | return tuple(relationships) 77 | 78 | 79 | def _get_relationships_old_django(model): 80 | """for django < 1.8""" 81 | relationships = [] 82 | for name in dir(model): 83 | attr = getattr(model, name) 84 | if isinstance(attr, SingleRelatedObjectDescriptor): 85 | relation_name = attr.related.model.__name__ 86 | relationships.append(Relationship(name, relation=relation_name)) 87 | elif isinstance(type(attr), RenameRelatedObjectDescriptorMethods) or\ 88 | isinstance(attr, ReverseManyRelatedObjectsDescriptor): 89 | relation_name = attr.field.rel.to.__name__ 90 | if isinstance(attr, ReverseManyRelatedObjectsDescriptor): 91 | relationships.append(ListRelationship(name, relation=relation_name)) 92 | else: 93 | relationships.append(Relationship(name, relation=relation_name)) 94 | elif isinstance(attr, (ForeignRelatedObjectsDescriptor, ManyRelatedObjectsDescriptor)): 95 | relation_name = attr.related.model.__name__ 96 | relationships.append(ListRelationship(name, relation=relation_name)) 97 | return tuple(relationships) 98 | 99 | 100 | def create_resource(model, resource_bases=(CRUDL,), 101 | relationships=None, links=None, preprocessors=None, 102 | postprocessors=None, fields=None, paginate_by=100, 103 | auto_relationships=True, pks=None, create_fields=None, 104 | update_fields=None, list_fields=None): 105 | """ 106 | Creates a ResourceBase subclass by inspecting a SQLAlchemy 107 | Model. This is somewhat more restrictive than explicitly 108 | creating managers and resources. However, if you only need 109 | any of the basic CRUD+L operations, 110 | 111 | :param sqlalchemy.Model model: This is the model that 112 | will be inspected to create a Resource and Manager from. 113 | By default, all of it's fields will be exposed, although 114 | this can be overridden using the fields attribute. 115 | :param tuple resource_bases: A tuple of ResourceBase subclasses. 116 | Defaults to the restmixins.CRUDL class only. However if you only 117 | wanted Update and Delete you could pass in 118 | ```(restmixins.Update, restmixins.Delete)``` which 119 | would cause the resource to inherit from those two. 120 | Additionally, you could create your own mixins and pass them in 121 | as the resource_bases 122 | :param tuple relationships: extra relationships to pass 123 | into the ResourceBase constructor. If auto_relationships 124 | is set to True, then they will be appended to these relationships. 125 | :param tuple links: Extra links to pass into the ResourceBase as 126 | the class _links attribute. Defaults to an empty tuple. 127 | :param tuple preprocessors: Preprocessors for the resource class attribute. 128 | :param tuple postprocessors: Postprocessors for the resource class attribute. 129 | :param tuple fields: The fields to expose on the api. Defaults to 130 | all of the fields on the model. 131 | :param bool auto_relationships: If True, then the SQLAlchemy Model 132 | will be inspected for relationships and they will be automatically 133 | appended to the relationships on the resource class attribute. 134 | :param list create_fields: A list of the fields that are valid when 135 | creating a resource. By default this will be the fields without 136 | any primary keys included 137 | :param list update_fields: A list of the fields that are valid when 138 | updating a resource. By default this will be the fields without 139 | any primary keys included 140 | :param list list_fields: A list of the fields that will be returned 141 | when the list endpoint is requested. Defaults to the fields 142 | attribute. 143 | :return: A ResourceBase subclass and DjangoManager subclass 144 | :rtype: ResourceMetaClass 145 | """ 146 | relationships = relationships or tuple() 147 | if auto_relationships: 148 | relationships += _get_relationships(model) 149 | links = links or tuple() 150 | preprocessors = preprocessors or tuple() 151 | postprocessors = postprocessors or tuple() 152 | pks = pks or _get_pks(model) 153 | fields = fields or _get_fields_for_model(model) 154 | list_fields = list_fields or fields 155 | 156 | create_fields = create_fields or [x for x in fields if x not in set(pks)] 157 | update_fields = update_fields or [x for x in fields if x not in set(pks)] 158 | 159 | manager_cls_attrs = dict(paginate_by=paginate_by, fields=fields, model=model, 160 | list_fields=list_fields, create_fields=create_fields, 161 | update_fields=update_fields) 162 | manager_class = type(str(model.__name__), (DjangoManager,), manager_cls_attrs) 163 | manager = manager_class() 164 | 165 | resource_cls_attrs = dict(preprocessors=preprocessors, 166 | postprocessors=postprocessors, 167 | _relationships=relationships, _links=links, 168 | pks=pks, manager=manager) 169 | res_class = ResourceMetaClass(str(model.__name__), resource_bases, resource_cls_attrs) 170 | return res_class 171 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-ripozo.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-ripozo.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /django_ripozo/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from django.db import models 7 | from django.db.models.query import QuerySet 8 | from django.db.models.manager import Manager 9 | from django.db.models.fields.related import ForeignObjectRel, ForeignKey 10 | 11 | from ripozo.exceptions import NotFoundException 12 | from ripozo.manager_base import BaseManager 13 | from ripozo.resources.fields.common import BaseField, StringField, \ 14 | BooleanField, FloatField, DateTimeField, IntegerField 15 | from ripozo.utilities import make_json_safe 16 | 17 | import logging 18 | import six 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | _COLUMN_FIELD_MAP = { 23 | models.IntegerField: IntegerField, 24 | models.BigIntegerField: IntegerField, 25 | models.AutoField: IntegerField, 26 | models.CharField: StringField, 27 | models.GenericIPAddressField: StringField, 28 | models.IPAddressField: StringField, 29 | models.DecimalField: StringField, 30 | models.DateTimeField: DateTimeField, 31 | models.DateField: DateTimeField, 32 | models.TimeField: DateTimeField, 33 | models.BooleanField: BooleanField, 34 | models.NullBooleanField: BooleanField, 35 | models.FloatField: FloatField, 36 | models.CommaSeparatedIntegerField: StringField, 37 | models.EmailField: StringField, 38 | models.PositiveSmallIntegerField: IntegerField, 39 | models.PositiveIntegerField: IntegerField, 40 | models.SlugField: StringField, 41 | models.SmallIntegerField: IntegerField, 42 | models.URLField: StringField 43 | } 44 | 45 | 46 | class DjangoManager(BaseManager): 47 | """ 48 | :param model: The django database model that this manager 49 | is responsible for handling 50 | :type model: django.db.models.Model 51 | """ 52 | pagination_pk_query_arg = 'page' 53 | 54 | @property 55 | def queryset(self): 56 | """ 57 | By default returns the self.model.objects 58 | If you wish to restrict the records available 59 | override this. 60 | """ 61 | return self.model.objects 62 | 63 | @staticmethod 64 | def _get_field_python_type(model, name): 65 | """ 66 | Gets the python type for the attribute on the model 67 | with the name provided. 68 | 69 | :param Model model: The Django model class. 70 | :param unicode name: The column name on the model 71 | that you are attempting to get the python type. 72 | :return: The python type of the column 73 | :rtype: type 74 | """ 75 | parts = name.split('.') 76 | for m in parts: 77 | if isinstance(model, ForeignKey): 78 | model = model.related_model 79 | if isinstance(model, ForeignObjectRel): 80 | model = model.model 81 | model = model._meta.get_field_by_name(m)[0] 82 | return model 83 | 84 | @classmethod 85 | def get_field_type(cls, name): 86 | """ 87 | :param unicode name: The name of the field to get the 88 | ripozo field type from. 89 | :return: The ripozo field type that most closely 90 | matches the database type. 91 | :rtype: ripozo.viewsets.fields.base.BaseField 92 | """ 93 | column = cls._get_field_python_type(cls.model, name) 94 | column_type = type(column) 95 | if column_type in _COLUMN_FIELD_MAP: 96 | field_class = _COLUMN_FIELD_MAP[column_type] 97 | return field_class(name) 98 | return BaseField(name) 99 | 100 | def create(self, values, *args, **kwargs): 101 | """ 102 | Creates a new model using the values provided. 103 | *args and **kwargs are ignored. 104 | 105 | :param dict values: A dictionary of the values to set 106 | on the new model that is being created 107 | :param list args: Ignored for now 108 | :param dict kwargs: Ignored for now 109 | :return: The new properties on the model, 110 | including the items on the model that are 111 | defaults 112 | :rtype: dict 113 | """ 114 | logger.info('Creating model {0} with values {1}'.format(self.model.__name__, values)) 115 | model = self.model() 116 | values = self.valid_fields(values, self.create_fields) 117 | model = self._set_fields_on_model(model, values) 118 | model.save() 119 | return self.serialize_model(model) 120 | 121 | def retrieve(self, lookup_keys, *args, **kwargs): 122 | """ 123 | Gets a model and selects the fields from the fields 124 | attribute on this manager to return in a dict. 125 | 126 | :param dict lookup_keys: The keys used to find the model 127 | to serialize 128 | :return: The serialized model in a dictionary form with just 129 | the fields specified in the manager. 130 | :rtype: dict 131 | """ 132 | logger.info('Retrieving an individual model {0} with lookup ' 133 | 'keys {1}'.format(self.model.__name__, lookup_keys)) 134 | model = self.get_model(lookup_keys) 135 | return self.serialize_model(model) 136 | 137 | def retrieve_list(self, filters, *args, **kwargs): 138 | """ 139 | Retrieves a list of records. 140 | 141 | :param dict filters: 142 | :return: 143 | :rtype: 144 | """ 145 | logger.info('Retrieving list of {0} with filters ' 146 | '{1}'.format(self.model.__name__, filters)) 147 | count = filters.pop(self.pagination_count_query_arg, self.paginate_by) 148 | page = filters.pop(self.pagination_pk_query_arg, 1) 149 | page -= 1 # Pages shouldn't be zero-indexed 150 | 151 | offset = page * count 152 | total = self.queryset.filter(**filters).count() 153 | queryset = self.queryset.filter(**filters)[offset:offset + count] 154 | 155 | prev_page = None 156 | next_page = None 157 | 158 | # Weird additions due to how it's actually exposed. 159 | if total > offset + count: 160 | next_page = page + 2 161 | if page > 0: 162 | prev_page = page 163 | links = dict() 164 | if prev_page: 165 | links.update(dict(prev={self.pagination_count_query_arg: count, self.pagination_pk_query_arg: prev_page})) 166 | if next_page: 167 | links.update(dict(next={self.pagination_count_query_arg: count, self.pagination_pk_query_arg: next_page})) 168 | 169 | props = self.serialize_model(queryset, fields=self.list_fields) 170 | return props, dict(links=links) 171 | 172 | def update(self, lookup_keys, updates, *args, **kwargs): 173 | """ 174 | Updates the model found with the lookup keys and returns 175 | the serialized model. 176 | 177 | :param dict lookup_keys: 178 | :param dict updates: 179 | :return: 180 | :rtype: dict 181 | :raises: NotFoundException 182 | """ 183 | logger.info('Updating model {0} with lookup keys {1}: values = ' 184 | '{2}'.format(self.model.__name__, lookup_keys, updates)) 185 | model = self.get_model(lookup_keys) 186 | updates = self.valid_fields(updates, self.update_fields) 187 | for key, value in six.iteritems(updates): 188 | setattr(model, key, value) 189 | model.save() 190 | return self.serialize_model(model) 191 | 192 | def delete(self, lookup_keys, *args, **kwargs): 193 | """ 194 | Deletes the model found with lookup_keys 195 | 196 | :param dict okup_keys: 197 | :return: Empty dict 198 | """ 199 | logger.info('Attempting to delete model {0} with lookup keys ' 200 | '{1}'.format(self.model.__name__, lookup_keys)) 201 | model = self.get_model(lookup_keys) 202 | model.delete() 203 | 204 | def get_model(self, lookup_keys): 205 | """ 206 | Retrieves a model with the specified lookup keys 207 | 208 | :param dict lookup_keys: The fields and attributes that 209 | uniquely identify the model 210 | :return: The model if found 211 | :rtype: django.db.models.Model 212 | :raises: NotFoundException 213 | """ 214 | try: 215 | return self.queryset.filter(**lookup_keys).get() 216 | except self.model.DoesNotExist: 217 | raise NotFoundException('No model of type {0} could be found using' 218 | ' lookup keys {1}'.format(self.model.__name__, lookup_keys)) 219 | 220 | def serialize_model(self, model, fields=None): 221 | """ 222 | :param model: The model or queryset that is being serialized. 223 | :type model: django.db.models.Model 224 | :param list fields: The list of fields to include in the serialization 225 | :return: The serialized model 226 | :rtype: dict 227 | """ 228 | fields = fields or self.fields 229 | field_dict = self.dot_field_list_to_dict(fields=fields) 230 | response = self._serialize_model_helper(model, field_dict=field_dict) 231 | return make_json_safe(response) 232 | 233 | def _serialize_model_helper(self, model, field_dict=None): 234 | # TODO docs 235 | if model is None: 236 | return None 237 | 238 | if isinstance(model, Manager): 239 | model = model.all() 240 | 241 | if isinstance(model, (QuerySet,)): 242 | model_list = [] 243 | for m in model: 244 | model_list.append(self._serialize_model_helper(m, field_dict=field_dict)) 245 | return model_list 246 | 247 | model_dict = {} 248 | for name, sub in six.iteritems(field_dict): 249 | value = getattr(model, name) 250 | if sub: 251 | value = self._serialize_model_helper(value, field_dict=sub) 252 | model_dict[name] = value 253 | return model_dict 254 | 255 | def _set_fields_on_model(self, model, values): 256 | """ 257 | Will set the values on the model if and only 258 | if they are present in the fields property. 259 | If they are not it will silently skip them. 260 | 261 | :param model: 262 | :type model: django.db.models.Model 263 | :param dict values: 264 | :return: The updated model 265 | :rtype: django.db.models.Model 266 | """ 267 | for name, value in six.iteritems(values): 268 | if name in self.fields: 269 | setattr(model, name, value) 270 | return model -------------------------------------------------------------------------------- /django_ripozo_tests/unit/manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | 6 | from datetime import datetime, time, date 7 | 8 | from decimal import Decimal 9 | 10 | from django.db.models.manager import Manager 11 | 12 | from django_ripozo.manager import DjangoManager 13 | 14 | from django_ripozo_tests.helpers.common import UnittestBase, profileit 15 | 16 | from ripozo.exceptions import NotFoundException 17 | from ripozo.resources.fields.common import StringField, BooleanField, FloatField, \ 18 | DateTimeField, IntegerField, BaseField 19 | 20 | from testapp.models import MyModel 21 | 22 | import mock 23 | import random 24 | import string 25 | import six 26 | import unittest2 27 | 28 | 29 | def random_string(): 30 | return ''.join(random.choice(string.ascii_letters) for _ in range(20)) 31 | 32 | 33 | def random_int(): 34 | return random.choice(range(100)) 35 | 36 | 37 | def random_bool(): 38 | return random.choice([True, False]) 39 | 40 | 41 | class TestDjangoManager(UnittestBase, unittest2.TestCase): 42 | def setUp(self): 43 | class MyMangaer(DjangoManager): 44 | model = MyModel 45 | fields = ['id', 'biginteger', 'boolean', 'char', 'csi', 46 | 'date_a', 'datetime_a', 'decimal_a', 47 | 'email', 'float_a', 'integer', 'ipaddress', 'genericip', 48 | 'nullbool', 'positiveint', 'positivesmallint', 'slug', 49 | 'smallint', 'time_a', 'url'] 50 | 51 | self.model = MyModel 52 | self.manager = MyMangaer 53 | super(TestDjangoManager, self).setUp() 54 | 55 | def assertJsonifiedDict(self, d1, d2): 56 | for key, value in six.iteritems(d1): 57 | response_value = d2[key] 58 | if isinstance(value, datetime): 59 | value = six.text_type(value) 60 | elif isinstance(value, time): 61 | value = value.strftime('%H:%M:%S') 62 | elif isinstance(value, date): 63 | value = value.strftime('%Y-%m-%d') 64 | self.assertEqual(response_value, value) 65 | 66 | def create_model(self): 67 | values = self.get_fields_dict() 68 | model = self.model(**values) 69 | model.save() 70 | return dict(id=model.id), values 71 | 72 | @property 73 | def field_type_dict(self): 74 | return dict(id=IntegerField, biginteger=IntegerField, boolean=BooleanField, 75 | char=StringField, csi=StringField, date_a=DateTimeField, datetime_a=DateTimeField, 76 | decimal_a=StringField, email=StringField, float_a=FloatField, 77 | integer=IntegerField, ipaddress=StringField, genericip=StringField, 78 | nullbool=BooleanField, positiveint=IntegerField, positivesmallint=IntegerField, 79 | slug=StringField, smallint=IntegerField, time_a=DateTimeField, url=StringField) 80 | 81 | def get_fields_dict(self): 82 | return dict( 83 | biginteger=random_int(), 84 | boolean=random_bool(), 85 | char=random_string(), 86 | csi=random_string(), 87 | date_a=date.today(), 88 | datetime_a=datetime.utcnow(), 89 | decimal_a=1.02, 90 | # duration=datetime.now(), 91 | email=random_string(), 92 | float_a=1.02, 93 | integer=random_int(), 94 | ipaddress=random_string(), 95 | genericip=random_string(), 96 | nullbool=random_bool(), 97 | positiveint=random_int(), 98 | positivesmallint=random_int(), 99 | slug=random_string(), 100 | smallint=random_int(), 101 | time_a=time(), 102 | url=random_string() 103 | ) 104 | 105 | def test_queryset_property(self): 106 | """ 107 | Tests the queryset property on the DjangoManager 108 | pretty basic 109 | """ 110 | queryset = self.manager().queryset 111 | self.assertIsInstance(queryset, Manager) 112 | 113 | class FakeManager(DjangoManager): 114 | pass 115 | 116 | try: 117 | FakeManager().queryset 118 | assert False 119 | except AttributeError: 120 | pass 121 | 122 | def test_get_field_type(self): 123 | """ 124 | Tests whether the appropriate field type is returned. 125 | """ 126 | for name, value in six.iteritems(self.field_type_dict): 127 | self.assertIsInstance(self.manager().get_field_type(name), value, 128 | msg='{0} does not return {1}'.format(name, value)) 129 | 130 | def test_create(self): 131 | """ 132 | Tests the creation of an object using the serializer. 133 | """ 134 | m = self.manager() 135 | value = self.get_fields_dict() 136 | response = m.create(value) 137 | self.assertJsonifiedDict(value, response) 138 | for f in self.manager.fields: 139 | self.assertIn(f, response) 140 | 141 | def test_retrieve(self): 142 | """ 143 | Tests retrieving an existing model 144 | """ 145 | lookup_keys, values = self.create_model() 146 | values.update(lookup_keys) 147 | response = self.manager().retrieve(lookup_keys) 148 | self.assertJsonifiedDict(values, response) 149 | 150 | def test_retrieve_404(self): 151 | """ 152 | Tests retrieving a model that does not 153 | exist. 154 | """ 155 | self.assertRaises(NotFoundException, self.manager().retrieve, dict(id=1040230)) 156 | 157 | @profileit 158 | def test_retrieve_all(self): 159 | """ 160 | Tests the retrieval of all available objects. 161 | """ 162 | vals, meta = self.manager().retrieve_list({}) 163 | original_length = len(vals) 164 | 165 | new_count = 100 166 | all_vals = [] 167 | for i in range(new_count): 168 | lookup, indv_vals = self.create_model() 169 | indv_vals.update(lookup) 170 | all_vals.append(indv_vals) 171 | vals, meta = self.manager().retrieve_list({}) 172 | self.assertEqual(len(vals), original_length + new_count) 173 | 174 | @profileit 175 | def test_pagination(self): 176 | """ 177 | Test retrieve_list with pagination. 178 | """ 179 | class NewManager(self.manager): 180 | paginate_by = 3 181 | 182 | vals, meta = self.manager().retrieve_list({}) 183 | original_length = len(vals) 184 | 185 | new_count = 100 186 | for i in range(new_count): 187 | self.create_model() 188 | 189 | vals, meta = NewManager().retrieve_list({}) 190 | total_retrieved = len(vals) 191 | while 'next' in meta['links']: 192 | vals, meta = NewManager().retrieve_list({ 193 | NewManager.pagination_count_query_arg: meta['links']['next'][NewManager.pagination_count_query_arg], 194 | NewManager.pagination_pk_query_arg: meta['links']['next'][NewManager.pagination_pk_query_arg] 195 | }) 196 | total_retrieved += len(vals) 197 | self.assertLessEqual(len(vals), NewManager.paginate_by) 198 | self.assertEqual(original_length + new_count, total_retrieved) 199 | 200 | @profileit 201 | def test_list_fields_retrieve_all(self): 202 | """ 203 | Checks to ensure that the list fields are used if 204 | they are available. 205 | """ 206 | class ListManager(self.manager): 207 | list_fields = ['id'] 208 | 209 | vals, meta = self.manager().retrieve_list({}) 210 | original_length = len(vals) 211 | 212 | new_count = 100 213 | for i in range(new_count): 214 | self.create_model() 215 | vals, meta = ListManager().retrieve_list({}) 216 | self.assertEqual(len(vals), original_length + new_count) 217 | 218 | for v in vals: 219 | self.assertEqual(len(v), 1) 220 | self.assertIn('id', v) 221 | 222 | def test_update(self): 223 | """ 224 | Simple check for updating. 225 | """ 226 | lookup, values = self.create_model() 227 | new_vals = self.get_fields_dict() 228 | vals = self.manager().update(lookup, new_vals) 229 | model = self.manager().get_model(lookup) 230 | for key, val in six.iteritems(new_vals): 231 | model_val = getattr(model, key) 232 | if isinstance(model_val, Decimal): 233 | model_val = float(model_val) 234 | self.assertEqual(model_val, val) 235 | 236 | def test_update_not_existing(self): 237 | """ 238 | Tests attempting to update an object that does not exist. 239 | """ 240 | self.assertRaises(NotFoundException, self.manager().update, {'id': -1}, {'hehe': 1}) 241 | 242 | def test_update_wrong_fields(self): 243 | """ 244 | Tests updating a field that is not in the fields list 245 | """ 246 | lookup, vals = self.create_model() 247 | vals.update(lookup) 248 | response = self.manager().update(lookup, {'notreal': 1}) 249 | model = self.manager().get_model(lookup) 250 | for key, val in six.iteritems(vals): 251 | model_val = getattr(model, key) 252 | if isinstance(model_val, Decimal): 253 | model_val = float(model_val) 254 | self.assertEqual(model_val, val) 255 | 256 | def test_delete(self): 257 | """ 258 | Tests simple delete 259 | """ 260 | lookup, vals = self.create_model() 261 | self.manager().delete(lookup) 262 | self.assertRaises(NotFoundException, self.manager().get_model, lookup) 263 | 264 | def test_delete_not_existing(self): 265 | """ 266 | Tests the deletion of a resource that 267 | does not exist. 268 | """ 269 | self.assertRaises(NotFoundException, self.manager().delete, {'id': -1}) 270 | 271 | def test_serialize_model_helper_none(self): 272 | """ 273 | Tests that the _serialize_model_helper 274 | returns None if the model is None. 275 | """ 276 | manager = self.manager() 277 | self.assertIsNone(manager._serialize_model_helper(None)) 278 | 279 | def test_get_field_type_not_found(self): 280 | """ 281 | Tests that a BaseField is returned if the 282 | model could not be found. 283 | """ 284 | mck = mock.MagicMock() 285 | 286 | class FakeManager(DjangoManager): 287 | model = mck 288 | 289 | self.assertIsInstance(FakeManager().get_field_type('blah'), BaseField) 290 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-ripozo documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Jun 16 14:46:53 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.ifconfig', 39 | 'sphinx.ext.viewcode', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = u'django-ripozo' 58 | copyright = u'2015, Tim Martin' 59 | author = u'Tim Martin' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '0.1.1' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '0.1.1' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = [] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | #add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = True 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | html_theme = 'alabaster' 120 | 121 | # Theme options are theme-specific and customize the look and feel of a theme 122 | # further. For a list of options available for each theme, see the 123 | # documentation. 124 | #html_theme_options = {} 125 | 126 | # Add any paths that contain custom themes here, relative to this directory. 127 | #html_theme_path = [] 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | #html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as html_title. 134 | #html_short_title = None 135 | 136 | # The name of an image file (relative to this directory) to place at the top 137 | # of the sidebar. 138 | #html_logo = None 139 | 140 | # The name of an image file (within the static path) to use as favicon of the 141 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 142 | # pixels large. 143 | #html_favicon = None 144 | 145 | # Add any paths that contain custom static files (such as style sheets) here, 146 | # relative to this directory. They are copied after the builtin static files, 147 | # so a file named "default.css" will overwrite the builtin "default.css". 148 | html_static_path = ['_static'] 149 | 150 | # Add any extra paths that contain custom files (such as robots.txt or 151 | # .htaccess) here, relative to this directory. These files are copied 152 | # directly to the root of the documentation. 153 | #html_extra_path = [] 154 | 155 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 156 | # using the given strftime format. 157 | #html_last_updated_fmt = '%b %d, %Y' 158 | 159 | # If true, SmartyPants will be used to convert quotes and dashes to 160 | # typographically correct entities. 161 | #html_use_smartypants = True 162 | 163 | # Custom sidebar templates, maps document names to template names. 164 | #html_sidebars = {} 165 | 166 | # Additional templates that should be rendered to pages, maps page names to 167 | # template names. 168 | #html_additional_pages = {} 169 | 170 | # If false, no module index is generated. 171 | #html_domain_indices = True 172 | 173 | # If false, no index is generated. 174 | #html_use_index = True 175 | 176 | # If true, the index is split into individual pages for each letter. 177 | #html_split_index = False 178 | 179 | # If true, links to the reST sources are added to the pages. 180 | #html_show_sourcelink = True 181 | 182 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 183 | #html_show_sphinx = True 184 | 185 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 186 | #html_show_copyright = True 187 | 188 | # If true, an OpenSearch description file will be output, and all pages will 189 | # contain a tag referring to it. The value of this option must be the 190 | # base URL from which the finished HTML is served. 191 | #html_use_opensearch = '' 192 | 193 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 194 | #html_file_suffix = None 195 | 196 | # Language to be used for generating the HTML full-text search index. 197 | # Sphinx supports the following languages: 198 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 199 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 200 | #html_search_language = 'en' 201 | 202 | # A dictionary with options for the search language support, empty by default. 203 | # Now only 'ja' uses this config value 204 | #html_search_options = {'type': 'default'} 205 | 206 | # The name of a javascript file (relative to the configuration directory) that 207 | # implements a search results scorer. If empty, the default will be used. 208 | #html_search_scorer = 'scorer.js' 209 | 210 | # Output file base name for HTML help builder. 211 | htmlhelp_basename = 'django-ripozodoc' 212 | 213 | # -- Options for LaTeX output --------------------------------------------- 214 | 215 | latex_elements = { 216 | # The paper size ('letterpaper' or 'a4paper'). 217 | #'papersize': 'letterpaper', 218 | 219 | # The font size ('10pt', '11pt' or '12pt'). 220 | #'pointsize': '10pt', 221 | 222 | # Additional stuff for the LaTeX preamble. 223 | #'preamble': '', 224 | 225 | # Latex figure (float) alignment 226 | #'figure_align': 'htbp', 227 | } 228 | 229 | # Grouping the document tree into LaTeX files. List of tuples 230 | # (source start file, target name, title, 231 | # author, documentclass [howto, manual, or own class]). 232 | latex_documents = [ 233 | (master_doc, 'django-ripozo.tex', u'django-ripozo Documentation', 234 | u'Tim Martin', 'manual'), 235 | ] 236 | 237 | # The name of an image file (relative to this directory) to place at the top of 238 | # the title page. 239 | #latex_logo = None 240 | 241 | # For "manual" documents, if this is true, then toplevel headings are parts, 242 | # not chapters. 243 | #latex_use_parts = False 244 | 245 | # If true, show page references after internal links. 246 | #latex_show_pagerefs = False 247 | 248 | # If true, show URL addresses after external links. 249 | #latex_show_urls = False 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #latex_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #latex_domain_indices = True 256 | 257 | 258 | # -- Options for manual page output --------------------------------------- 259 | 260 | # One entry per manual page. List of tuples 261 | # (source start file, name, description, authors, manual section). 262 | man_pages = [ 263 | (master_doc, 'django-ripozo', u'django-ripozo Documentation', 264 | [author], 1) 265 | ] 266 | 267 | # If true, show URL addresses after external links. 268 | #man_show_urls = False 269 | 270 | 271 | # -- Options for Texinfo output ------------------------------------------- 272 | 273 | # Grouping the document tree into Texinfo files. List of tuples 274 | # (source start file, target name, title, author, 275 | # dir menu entry, description, category) 276 | texinfo_documents = [ 277 | (master_doc, 'django-ripozo', u'django-ripozo Documentation', 278 | author, 'django-ripozo', 'One line description of project.', 279 | 'Miscellaneous'), 280 | ] 281 | 282 | # Documents to append as an appendix to all manuals. 283 | #texinfo_appendices = [] 284 | 285 | # If false, no module index is generated. 286 | #texinfo_domain_indices = True 287 | 288 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 289 | #texinfo_show_urls = 'footnote' 290 | 291 | # If true, do not generate a @detailmenu in the "Top" node's menu. 292 | #texinfo_no_detailmenu = False 293 | 294 | 295 | # Example configuration for intersphinx: refer to the Python standard library. 296 | intersphinx_mapping = {'https://docs.python.org/': None} 297 | -------------------------------------------------------------------------------- /django_ripozo/dispatcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | from __future__ import unicode_literals 5 | __author__ = 'Tim Martin' 6 | 7 | from django.http import HttpResponse, QueryDict 8 | from django.conf.urls import url 9 | 10 | from django_ripozo.exceptions import MethodNotAllowed 11 | 12 | from functools import wraps 13 | 14 | from ripozo.dispatch_base import DispatcherBase 15 | from ripozo.exceptions import RestException 16 | from ripozo.resources.request import RequestContainer 17 | from ripozo.utilities import join_url_parts 18 | 19 | import re 20 | import six 21 | 22 | def _csrf_wrapper(f): 23 | @wraps(f) 24 | def wrapped(*args, **kwargs): 25 | return f(*args, **kwargs) 26 | wrapped.csrf_exempt = True 27 | return wrapped 28 | 29 | 30 | def default_error_handler(dispatcher, request, adapter_class, exc): 31 | """ 32 | The default error handler used by the method router 33 | when there is an error in the application. This provides 34 | a convienent place to appropriately handle exceptions. 35 | Perhaps just reraising all exceptions. 36 | 37 | :param DjangoDispatcher dispatcher: The dispatcher that 38 | was used to dispatch this method. Not used by 39 | this handler. 40 | :param ripozo.RequestContainer request: The request that 41 | caused the exception. Not used by this handler. 42 | :param ripozo.AdapterBase adapter_class: The adapter 43 | :param Exception exc: 44 | :return: The correctly formatted status. 45 | :rtype: HttpResponse 46 | """ 47 | if isinstance(exc, RestException): 48 | body, content_type, status_code = adapter_class.format_exception(exc) 49 | return HttpResponse(body, status=status_code, content_type=content_type) 50 | raise exc 51 | 52 | 53 | class MethodRouter(object): 54 | """ 55 | This is a callable object that is responsible 56 | for calling the specific method responsible for 57 | handling the http verb that was used. This is because 58 | Django does not have a manner of directing different 59 | HTTP verbs to different methods. 60 | """ 61 | _method_map = None 62 | 63 | def __init__(self, route, dispatcher, error_handler=default_error_handler): 64 | """ 65 | :param unicode route: 66 | :param DjangoDispatcher dispatcher: 67 | :param function error_handler: A function that takes the dispatcher, 68 | request, adapter base class, and an exception that was raised when 69 | dispatching the request. 70 | :return: 71 | :rtype: 72 | """ 73 | self.route = route 74 | self.dispatcher = dispatcher 75 | self.error_handler = error_handler 76 | 77 | def add_route(self, endpoint_func=None, endpoint=None, methods=None, **options): 78 | """ 79 | Adds a function to call and the http methods that will 80 | correspond to it. Currently, the endpoint and options are ignored. 81 | 82 | :param function endpoint_func: The function to be called when the route 83 | is called with one of the methods specified. 84 | :param unicode endpoint: Not used currently 85 | :param list methods: A list of the unicode methods that 86 | correspond to the endpoint_func. They are case insensitive. 87 | :param dict options: Not used currently. 88 | """ 89 | for method in methods: 90 | method = method.lower() 91 | if method in self.method_map: 92 | raise ValueError('The method {0} is already registered for the route ' 93 | '{1}'.format(method, self.route)) 94 | self.method_map[method.lower()] = endpoint_func 95 | 96 | @_csrf_wrapper 97 | def __call__(self, django_request, **url_parameters): 98 | """ 99 | This is a call to a django method. 100 | 101 | :param django.http.HttpRequest django_request: The django 102 | request object. 103 | :param dict url_parameters: The named url parameters 104 | :return: The django HttpResponse 105 | :rtype: django.http.HttpResponse 106 | """ 107 | accepted_mimetypes = django_request.META.get('HTTP_ACCEPT', []) 108 | adapter_class = self.dispatcher.get_adapter_for_type(accepted_mimetypes) 109 | body_parameters = QueryDict(django_request.body) 110 | request = DjangoRequestContainer(django_request, url_params=url_parameters, 111 | query_args=dict(django_request.GET), body_args=dict(body_parameters), 112 | headers=dict(django_request.META)) 113 | try: 114 | endpoint_func = self.get_func_for_method(django_request.method) 115 | adapter = self.dispatcher.dispatch(endpoint_func, accepted_mimetypes, request) 116 | except Exception as e: 117 | return self.error_handler(self.dispatcher, request, adapter_class, e) 118 | response = HttpResponse(adapter.formatted_body, status=adapter.status_code) 119 | for header, value in six.iteritems(adapter.extra_headers): 120 | response[header] = value 121 | return response 122 | 123 | @property 124 | def method_map(self): 125 | """ 126 | :return: The dictionary of the HTTP methods 127 | and their corresponding endpoint functions. 128 | :rtype: dict 129 | """ 130 | if self._method_map is None: 131 | self._method_map = {} 132 | return self._method_map 133 | 134 | def get_func_for_method(self, http_method): 135 | """ 136 | :param unicode http_method: The http verb 137 | :return: The method corresponding to the http verb 138 | :rtype: types.MethodType 139 | """ 140 | http_method = http_method.lower() 141 | try: 142 | return self.method_map[http_method] 143 | except KeyError: 144 | raise MethodNotAllowed('The method {0} is not available for ' 145 | 'the route {1}'.format(http_method, self.route)) 146 | 147 | _url_parameter_finder = re.compile(r'<(.+?)>') 148 | 149 | 150 | class DjangoDispatcher(DispatcherBase): 151 | # TODO docs 152 | _url_map = None 153 | _routers = None 154 | 155 | def __init__(self, base_url='', method_route_class=MethodRouter, 156 | error_handler=default_error_handler, **kwargs): 157 | self._base_url = base_url 158 | self.method_route_class = method_route_class 159 | self.error_handler = error_handler 160 | super(DjangoDispatcher, self).__init__(**kwargs) 161 | 162 | @property 163 | def url_map(self): 164 | """ 165 | :return: A dictionary of the unicode routes and 166 | the MethodRouters that correspond to that route. 167 | :rtype: dict 168 | """ 169 | if self._url_map is None: 170 | self._url_map = {} 171 | return self._url_map 172 | 173 | @property 174 | def base_url(self): 175 | """ 176 | :return: The base part of the url that will 177 | be prepended to all routes. For example, 178 | you might use '/api' to dictate what every 179 | url should be prepended with. 180 | :rtype: unicode 181 | """ 182 | return self._base_url 183 | 184 | def register_route(self, endpoint, endpoint_func=None, route=None, methods=None, **options): 185 | """ 186 | Adds a route to the url_map which is used when getting 187 | the url_patterns property which are then added to the django 188 | app. 189 | 190 | :param unicode endpoint: The name of this endpoint 191 | :param function endpoint_func: the function that should be called when 192 | this endpoint is hit. 193 | :param unicode route: The url that corresponds to this endpoint. 194 | Each unique url generates a MethodRouter which then dispatches 195 | them to the appopriate endpoints for the http verbs 196 | :param list methods: The http verbs that correspond to this endpoint 197 | :param dict options: Additional options. Not used at this time 198 | """ 199 | route = join_url_parts(self.base_url, route) 200 | route = self._convert_url_to_regex(route) 201 | if route not in self.url_map: 202 | self.url_map[route] = self.method_route_class(route, self, self.error_handler) 203 | method_router = self.url_map[route] 204 | method_router.add_route(endpoint_func=endpoint_func, endpoint=endpoint, 205 | methods=methods, **options) 206 | 207 | @property 208 | def url_patterns(self): 209 | """ 210 | :return: A list of tuples built according to the 211 | ``django.conf.urls.patterns`` method. The 212 | first argument is the base_url that will 213 | be prepended. 214 | :rtype: list 215 | """ 216 | urls = [] 217 | for router in six.itervalues(self.url_map): 218 | urls.append(url(router.route, router)) 219 | return urls 220 | 221 | @staticmethod 222 | def _convert_url_to_regex(route): 223 | """ 224 | Takes a ripozo formatted url (one with the url 225 | parameters enclosed in angle brackets such as ``'/myresource//'``) 226 | and returns a django regex formatted url: '^myresource/(?P[^/]+)/$'. 227 | It will also append the ^ character to the beggining and $ character 228 | to the end. 229 | 230 | :param unicode route: 231 | :return: A django formatted url regex pattern. 232 | :rtype: unicode 233 | """ 234 | url_parameters = set(_url_parameter_finder.findall(route)) 235 | for param in url_parameters: 236 | old_param = '<{0}>'.format(param) 237 | new_param = '(?P<{0}>[^/]+)'.format(param) 238 | route = route.replace(old_param, new_param) 239 | route = route.lstrip('/') 240 | return '^{0}$'.format(route) 241 | 242 | def dispatch(self, endpoint_func, accepted_mimetypes, *args, **kwargs): 243 | """ 244 | A helper to dispatch the endpoint_func, get the ResourceBase 245 | subclass instance, get the appropriate AdapterBase subclass 246 | and return an instance created with the ResourceBase. 247 | 248 | :param method endpoint_func: The endpoint_func is responsible 249 | for actually get the ResourceBase response 250 | :param list accepted_mimetypes: The mime types accepted by 251 | the client. If none of the mimetypes provided are 252 | available the default adapter will be used. 253 | :param list args: a list of args that wll be passed 254 | to the endpoint_func 255 | :param dict kwargs: a dictionary of keyword args to 256 | pass to the endpoint_func 257 | :return: an instance of an AdapterBase subclass that 258 | can be used to find 259 | :rtype: 260 | """ 261 | result = endpoint_func(*args, **kwargs) 262 | request = args[0] 263 | base_url = request.django_request.build_absolute_uri(self.base_url) 264 | adapter_class = self.get_adapter_for_type(accepted_mimetypes) 265 | adapter = adapter_class(result, base_url=base_url) 266 | return adapter 267 | 268 | 269 | class DjangoRequestContainer(RequestContainer): 270 | def __init__(self, request, *args, **kwargs): 271 | self.django_request = request 272 | super(DjangoRequestContainer, self).__init__(*args, **kwargs) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | --------------------------------------------------------------------------------