Skip to content

Commit f526cce

Browse files
committed
Merge pull request #1 from juannyg/0.1dev
0.1dev
2 parents 05d8ab9 + 307f5cc commit f526cce

File tree

20 files changed

+876
-8
lines changed

20 files changed

+876
-8
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.pyc
2+
*.db
3+
*~
4+
.*
5+
*.egg-info/
6+
!.gitignore

LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2014 Juan Gutierrez
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include *.md, *.txt

README.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,195 @@
1+
Django REST Framework SaaS Plugin
2+
=================================
3+
4+
### Overview
5+
6+
This is a SaaS driven plugin for Django REST Framework. It offers a simple way
7+
to separate client customizations for your core API web services. Currently, this
8+
initial version only supports client API routing via ViewSets in conjunction with
9+
an extension of the Django REST Framework SimpleRouter. Future releases will
10+
have broader coverage of DRF features for custom client routing.
11+
12+
### Install
13+
```pip install djangorestframework-saasy```
14+
15+
### Requirements
16+
- Python (2.7)
17+
- Django (1.4.2+)
18+
- Django rest framework (2.3.14+)
19+
20+
### Example
21+
22+
Define the client model in your django rest framework settings:
23+
```python
24+
REST_SETTINGS = {
25+
...
26+
"SAAS": {
27+
"MODEL": "path.to.model.ClientModel"
28+
}
29+
...
30+
}
31+
```
32+
33+
Use the saas client mixin provided by the SaaS plugin, and define the required class methods:
34+
```python
35+
from django.db import models
36+
from rest_framework_saasy.client import ClientMixin
37+
38+
39+
class ClientModel(models.Model, ClientMixin):
40+
"""client model"""
41+
name = models.CharField(max_length=128)
42+
43+
@staticmethod
44+
def saas_lookup_field():
45+
"""DRF-SaaS lookup field definition"""
46+
return 'name'
47+
48+
def saas_client_module(self, saas_url_kw, *args, **kwargs):
49+
return 'customizations.{}'.format(self.name)
50+
```
51+
52+
#### ClientMixin methods
53+
54+
- *saas_lookup_field* **[required]**
55+
56+
This method defines what field in your client model to use when looking up
57+
the client in the database, to verify they exist.
58+
59+
- *saas_client_module* **[required]**
60+
61+
Parameters:
62+
- *saas_url_kw* - [string] - value from URL key word argument - the client
63+
idenfitication value.
64+
65+
All client code should be separated from the core and also from each other.
66+
A good practice to follow is that there is a folder for all client specific code,
67+
separate from the core, with a folder for each client. That said, you can impose
68+
any kind of path rules you wish.
69+
70+
```
71+
project
72+
├── customizations
73+
│ └── client_name
74+
│ └── app
75+
│ └── subpackage
76+
│ └── module.py
77+
└── app
78+
└── subpackage
79+
└── module.py
80+
```
81+
82+
### ViewSets
83+
84+
The idea is there is a core web service ViewSet, *WebService*, defined
85+
in **app.subpackage.module** and in **customizations.client_name.app.subpackage.module**
86+
where there is also a class named *WebService*
87+
88+
**app.subpackage.module**
89+
```python
90+
from rest_framework import viewsets
91+
from rest_framework_saasy import viewsets as saas_viewsets
92+
93+
from .models import WebServiceModel
94+
from .serializers import WebServiceSerializer
95+
96+
class WebService(saas_viewsets.ViewSetMixin, viewsets.ModelViewSet):
97+
queryset = WebServiceModel.objects.all()
98+
serializer_class = WebServiceSerializer
99+
```
100+
101+
**customizations.client.app.subpackage.module**
102+
```python
103+
from app.subpackage.module import WebService as CoreWebService
104+
105+
class WebService(CoreWebService):
106+
# client customizations
107+
```
108+
109+
You can define the module path of client code and you can also define the subpackage
110+
path for the ViewSet mixed with the *saas_viewsets.ViewSetMixin*.
111+
112+
What cannot be customized is the name of the class - the class name *WebService* in the
113+
core must be defined identically in the client custom module.
114+
115+
#### ViewSetMixin methods
116+
117+
- *saas_module* **[optional]**
118+
119+
By default, viewset will be routed in a similar way as in the diagram above:
120+
121+
```
122+
project
123+
├── customizations
124+
│ └── client_name
125+
│ └── app
126+
│ └── subpackage
127+
│ └── module.py
128+
└── app
129+
└── subpackage
130+
└── module.py
131+
```
132+
133+
However, the SaaS viewset has an optional method that can be defined, *saas_module*
134+
This returns the path that should be used in the client package. **It must be
135+
defined with the staticmethod decorator.** Let's slightly alter our *WebService* example above:
136+
137+
```python
138+
class WebService(saas_viewsets.ViewSetMixin, viewsets.ModelViewSet):
139+
...
140+
@staticmethod
141+
def saas_module():
142+
return 'other.package.name'
143+
```
144+
145+
The expected file system package defintion for *WebService* customizations would be:
146+
147+
```
148+
project
149+
├── customizations
150+
│ └── client_name
151+
│ └── other
152+
│ └── package
153+
│ └── name.py
154+
└── app
155+
└── subpackage
156+
└── module.py
157+
```
158+
159+
#### ViewSet attributes
160+
161+
*saas_url_kw* is a new attribute made available to the ViewSet instance.
162+
The value of the valid identifier from the URL key word argument can be
163+
accessed at any time. If no client specific route was used, *saas_url_kw*
164+
defaults to None.
165+
166+
### SaaS SimpleRouter
167+
168+
You'll register your new SaaSy viewsets in exactly the same way Django
169+
REST Framework defines.
170+
171+
#### app.urls
172+
```python
173+
from rest_framework_saasy import routers
174+
from .views import NoteViewSet
175+
176+
177+
router = routers.SimpleRouter()
178+
router.register(r'notes', NoteViewSet)
179+
```
180+
181+
Client specific routes will be made available immediately:
182+
```
183+
^notes/$ [name='note-list']
184+
^notes/(?P<pk>[^/]+)/$ [name='note-detail']
185+
^(?P<saas_url_kw>.*)/notes/$ [name='note-list']
186+
^(?P<saas_url_kw>.*)/notes/(?P<pk>[^/]+)/$ [name='note-detail']
187+
```
188+
189+
**Note:** If a client key word argument is provided, but the client cannot
190+
be retreived from the database with the given identifier, the
191+
plugin will simply return a 404.
192+
1193
License
2194
=======
3195
The MIT License (MIT)

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
Django>=1.3
22
djangorestframework>=2.3.14
3+
mock==1.0.1
4+
simplejson==3.6.0

rest_framework_saasy/__init__.py

Whitespace-only changes.

rest_framework_saasy/client.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""SaaS Client Mixin"""
2+
3+
4+
class ClientMixin(object):
5+
"""
6+
To be mixed with the relevant model associated with a "customer" of
7+
your platform/service(s) - the settings value of:
8+
REST_SETTINGS = {
9+
...
10+
"SAAS": {
11+
"MODEL": "app.client.Model"
12+
}
13+
...
14+
}
15+
16+
The ClientMixin class dictates the implementation rules. There
17+
is no functionality defined here.
18+
"""
19+
20+
def saas_lookup_field(self, *args, **kwargs):
21+
"""Define the model lookup field to use when querying the database
22+
for the client record"""
23+
raise NotImplementedError
24+
25+
def saas_client_module(self, saas_url_kw, *args, **kwargs):
26+
"""Define module path to client customization"""
27+
raise NotImplementedError

rest_framework_saasy/routers.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
SaaS Router customizations
3+
"""
4+
from rest_framework import routers
5+
6+
SAAS_URL_KW = "saas_url_kw"
7+
_SAAS_URL_REGEX = "(?P<{}>.*)".format(SAAS_URL_KW)
8+
9+
LIST_ROUTE_ARGS = {
10+
'mapping': {'get': 'list', 'post': 'create'},
11+
'name': '{basename}-list',
12+
'initkwargs': {'suffix': 'List'}
13+
}
14+
15+
DETAIL_ROUTE_ARGS = {
16+
'mapping': {
17+
'get': 'retrieve',
18+
'put': 'update',
19+
'patch': 'partial_update',
20+
'delete': 'destroy'
21+
},
22+
'name': '{basename}-detail',
23+
'initkwargs': {'suffix': 'Instance'}
24+
}
25+
26+
METHOD_ROUTE_ARGS = {
27+
'mapping': {'{httpmethod}': '{methodname}'},
28+
'name': '{basename}-{methodnamehyphen}',
29+
'initkwargs': {}
30+
}
31+
32+
33+
class SimpleRouter(routers.SimpleRouter):
34+
"""
35+
SimpleRouter for SaaS
36+
"""
37+
routes = [
38+
# Default routes
39+
routers.Route(
40+
url=r'^{prefix}{trailing_slash}$',
41+
**LIST_ROUTE_ARGS
42+
),
43+
routers.Route(
44+
url=r'^{prefix}/{lookup}{trailing_slash}$',
45+
**DETAIL_ROUTE_ARGS
46+
),
47+
routers.Route(
48+
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
49+
**METHOD_ROUTE_ARGS
50+
),
51+
# Client specific routes...
52+
routers.Route(
53+
url=r'^{0}/{{prefix}}{{trailing_slash}}$'.format(_SAAS_URL_REGEX),
54+
**LIST_ROUTE_ARGS
55+
),
56+
routers.Route(
57+
url=r'^{0}/{{prefix}}/{{lookup}}{{trailing_slash}}$'.format(_SAAS_URL_REGEX),
58+
**DETAIL_ROUTE_ARGS
59+
),
60+
routers.Route(
61+
url=r'^{0}/{{prefix}}/{{lookup}}/{{methodname}}{{trailing_slash}}$'.format(_SAAS_URL_REGEX),
62+
**METHOD_ROUTE_ARGS
63+
)
64+
]

rest_framework_saasy/runtests/__init__.py

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/usr/bin/env python
2+
3+
# https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/runtests/runtests.py
4+
# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
5+
# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
6+
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
7+
import os
8+
import sys
9+
10+
# fix sys path so we don't need to setup PYTHONPATH
11+
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
12+
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework_saasy.runtests.settings'
13+
14+
import django
15+
from django.conf import settings
16+
from django.test.utils import get_runner
17+
18+
19+
def usage():
20+
return """
21+
Usage: python runtests.py [UnitTestClass].[method]
22+
23+
You can pass the Class name of the `UnitTestClass` you want to test.
24+
25+
Append a method name if you only want to test a specific method of that class.
26+
"""
27+
28+
29+
def main():
30+
try:
31+
django.setup()
32+
except AttributeError:
33+
pass
34+
TestRunner = get_runner(settings)
35+
36+
test_runner = TestRunner()
37+
if len(sys.argv) == 2:
38+
test_case = '.' + sys.argv[1]
39+
elif len(sys.argv) == 1:
40+
test_case = ''
41+
else:
42+
print(usage())
43+
sys.exit(1)
44+
45+
test_module_name = 'rest_framework_saasy.tests'
46+
if django.VERSION[0] == 1 and django.VERSION[1] < 6:
47+
test_module_name = 'tests'
48+
49+
if sys.argv[0] == 'setup.py':
50+
test_case = ''
51+
failures = test_runner.run_tests([test_module_name + test_case])
52+
53+
sys.exit(failures)
54+
55+
if __name__ == '__main__':
56+
main()

0 commit comments

Comments
 (0)