Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
D
django-rest-framework
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
django-rest-framework
Commits
6e51e4f5
Commit
6e51e4f5
authored
Dec 16, 2014
by
Tom Christie
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Versioning first pass
parent
b6ee7842
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
249 additions
and
7 deletions
+249
-7
docs/api-guide/versioning.md
+10
-0
rest_framework/reverse.py
+12
-0
rest_framework/settings.py
+5
-2
rest_framework/versioning.py
+96
-0
rest_framework/views.py
+22
-5
tests/test_versioning.py
+104
-0
No files found.
docs/api-guide/versioning.md
0 → 100644
View file @
6e51e4f5
source: versioning.py
# Versioning
> Versioning an interface is just a "polite" way to kill deployed clients.
>
> — [Roy Fielding][cite].
[
cite
]:
http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
\ No newline at end of file
rest_framework/reverse.py
View file @
6e51e4f5
...
@@ -9,6 +9,18 @@ from django.utils.functional import lazy
...
@@ -9,6 +9,18 @@ from django.utils.functional import lazy
def
reverse
(
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
def
reverse
(
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
"""
"""
If versioning is being used then we pass any `reverse` calls through
to the versioning scheme instance, so that the resulting URL
can be modified if needed.
"""
scheme
=
getattr
(
request
,
'versioning_scheme'
,
None
)
if
scheme
is
not
None
:
return
scheme
.
reverse
(
viewname
,
args
,
kwargs
,
request
,
format
,
**
extra
)
return
_reverse
(
viewname
,
args
,
kwargs
,
request
,
format
,
**
extra
)
def
_reverse
(
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
and returns a fully qualified URL, using the request to get the base URL.
"""
"""
...
...
rest_framework/settings.py
View file @
6e51e4f5
...
@@ -46,6 +46,7 @@ DEFAULTS = {
...
@@ -46,6 +46,7 @@ DEFAULTS = {
'DEFAULT_THROTTLE_CLASSES'
:
(),
'DEFAULT_THROTTLE_CLASSES'
:
(),
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
:
'rest_framework.negotiation.DefaultContentNegotiation'
,
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
:
'rest_framework.negotiation.DefaultContentNegotiation'
,
'DEFAULT_METADATA_CLASS'
:
'rest_framework.metadata.SimpleMetadata'
,
'DEFAULT_METADATA_CLASS'
:
'rest_framework.metadata.SimpleMetadata'
,
'DEFAULT_VERSIONING_CLASS'
:
None
,
# Generic view behavior
# Generic view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS'
:
'rest_framework.serializers.ModelSerializer'
,
'DEFAULT_MODEL_SERIALIZER_CLASS'
:
'rest_framework.serializers.ModelSerializer'
,
...
@@ -124,7 +125,7 @@ IMPORT_STRINGS = (
...
@@ -124,7 +125,7 @@ IMPORT_STRINGS = (
'DEFAULT_THROTTLE_CLASSES'
,
'DEFAULT_THROTTLE_CLASSES'
,
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
,
'DEFAULT_CONTENT_NEGOTIATION_CLASS'
,
'DEFAULT_METADATA_CLASS'
,
'DEFAULT_METADATA_CLASS'
,
'DEFAULT_
MODEL_SERIALIZER
_CLASS'
,
'DEFAULT_
VERSIONING
_CLASS'
,
'DEFAULT_PAGINATION_SERIALIZER_CLASS'
,
'DEFAULT_PAGINATION_SERIALIZER_CLASS'
,
'DEFAULT_FILTER_BACKENDS'
,
'DEFAULT_FILTER_BACKENDS'
,
'EXCEPTION_HANDLER'
,
'EXCEPTION_HANDLER'
,
...
@@ -141,7 +142,9 @@ def perform_import(val, setting_name):
...
@@ -141,7 +142,9 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
If the given setting is a string import notation,
then perform the necessary import or imports.
then perform the necessary import or imports.
"""
"""
if
isinstance
(
val
,
six
.
string_types
):
if
val
is
None
:
return
None
elif
isinstance
(
val
,
six
.
string_types
):
return
import_from_string
(
val
,
setting_name
)
return
import_from_string
(
val
,
setting_name
)
elif
isinstance
(
val
,
(
list
,
tuple
)):
elif
isinstance
(
val
,
(
list
,
tuple
)):
return
[
import_from_string
(
item
,
setting_name
)
for
item
in
val
]
return
[
import_from_string
(
item
,
setting_name
)
for
item
in
val
]
...
...
rest_framework/versioning.py
0 → 100644
View file @
6e51e4f5
# coding: utf-8
from
__future__
import
unicode_literals
from
rest_framework.reverse
import
_reverse
from
rest_framework.utils.mediatypes
import
_MediaType
import
re
class
BaseVersioning
(
object
):
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
msg
=
'{cls}.determine_version() must be implemented.'
raise
NotImplemented
(
msg
.
format
(
cls
=
self
.
__class__
.
__name__
))
def
reverse
(
self
,
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
return
_reverse
(
viewname
,
args
,
kwargs
,
request
,
format
,
**
extra
)
class
QueryParameterVersioning
(
BaseVersioning
):
"""
GET /something/?version=0.1 HTTP/1.1
Host: example.com
Accept: application/json
"""
default_version
=
None
version_param
=
'version'
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
return
request
.
query_params
.
get
(
self
.
version_param
)
def
reverse
(
self
,
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
url
=
super
(
QueryParameterVersioning
,
self
)
.
reverse
(
viewname
,
args
,
kwargs
,
request
,
format
,
**
kwargs
)
if
request
.
version
is
not
None
:
return
replace_query_param
(
url
,
self
.
version_param
,
request
.
version
)
return
url
class
HostNameVersioning
(
BaseVersioning
):
"""
GET /something/ HTTP/1.1
Host: v1.example.com
Accept: application/json
"""
default_version
=
None
hostname_regex
=
re
.
compile
(
r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$'
)
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
hostname
,
seperator
,
port
=
request
.
get_host
()
.
partition
(
':'
)
match
=
self
.
hostname_regex
.
match
(
hostname
)
if
not
match
:
return
self
.
default_version
return
match
.
group
(
1
)
# We don't need to implement `reverse`, as the hostname will already be
# preserved as part of the standard `reverse` implementation.
class
AcceptHeaderVersioning
(
BaseVersioning
):
"""
GET /something/ HTTP/1.1
Host: example.com
Accept: application/json; version=1.0
"""
default_version
=
None
version_param
=
'version'
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
media_type
=
_MediaType
(
request
.
accepted_media_type
)
return
media_type
.
params
.
get
(
self
.
version_param
,
self
.
default_version
)
# We don't need to implement `reverse`, as the versioning is based
# on the `Accept` header, not on the request URL.
class
URLPathVersioning
(
BaseVersioning
):
"""
GET /1.0/something/ HTTP/1.1
Host: example.com
Accept: application/json
"""
default_version
=
None
version_param
=
'version'
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
return
kwargs
.
get
(
self
.
version_param
,
self
.
default_version
)
def
reverse
(
self
,
viewname
,
args
=
None
,
kwargs
=
None
,
request
=
None
,
format
=
None
,
**
extra
):
if
request
.
version
is
not
None
:
kwargs
=
{}
if
(
kwargs
is
None
)
else
kwargs
kwargs
[
self
.
version_param
]
=
request
.
version
return
super
(
URLPathVersioning
,
self
)
.
reverse
(
viewname
,
args
,
kwargs
,
request
,
format
,
**
kwargs
)
rest_framework/views.py
View file @
6e51e4f5
...
@@ -95,6 +95,7 @@ class APIView(View):
...
@@ -95,6 +95,7 @@ class APIView(View):
permission_classes
=
api_settings
.
DEFAULT_PERMISSION_CLASSES
permission_classes
=
api_settings
.
DEFAULT_PERMISSION_CLASSES
content_negotiation_class
=
api_settings
.
DEFAULT_CONTENT_NEGOTIATION_CLASS
content_negotiation_class
=
api_settings
.
DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class
=
api_settings
.
DEFAULT_METADATA_CLASS
metadata_class
=
api_settings
.
DEFAULT_METADATA_CLASS
versioning_class
=
api_settings
.
DEFAULT_VERSIONING_CLASS
# Allow dependency injection of other settings to make testing easier.
# Allow dependency injection of other settings to make testing easier.
settings
=
api_settings
settings
=
api_settings
...
@@ -314,6 +315,16 @@ class APIView(View):
...
@@ -314,6 +315,16 @@ class APIView(View):
if
not
throttle
.
allow_request
(
request
,
self
):
if
not
throttle
.
allow_request
(
request
,
self
):
self
.
throttled
(
request
,
throttle
.
wait
())
self
.
throttled
(
request
,
throttle
.
wait
())
def
determine_version
(
self
,
request
,
*
args
,
**
kwargs
):
"""
If versioning is being used, then determine any API version for the
incoming request. Returns a two-tuple of (version, versioning_scheme)
"""
if
self
.
versioning_class
is
None
:
return
(
None
,
None
)
scheme
=
self
.
versioning_class
()
return
(
scheme
.
determine_version
(
request
,
*
args
,
**
kwargs
),
scheme
)
# Dispatch methods
# Dispatch methods
def
initialize_request
(
self
,
request
,
*
args
,
**
kwargs
):
def
initialize_request
(
self
,
request
,
*
args
,
**
kwargs
):
...
@@ -322,11 +333,13 @@ class APIView(View):
...
@@ -322,11 +333,13 @@ class APIView(View):
"""
"""
parser_context
=
self
.
get_parser_context
(
request
)
parser_context
=
self
.
get_parser_context
(
request
)
return
Request
(
request
,
return
Request
(
parsers
=
self
.
get_parsers
(),
request
,
authenticators
=
self
.
get_authenticators
(),
parsers
=
self
.
get_parsers
(),
negotiator
=
self
.
get_content_negotiator
(),
authenticators
=
self
.
get_authenticators
(),
parser_context
=
parser_context
)
negotiator
=
self
.
get_content_negotiator
(),
parser_context
=
parser_context
)
def
initial
(
self
,
request
,
*
args
,
**
kwargs
):
def
initial
(
self
,
request
,
*
args
,
**
kwargs
):
"""
"""
...
@@ -343,6 +356,10 @@ class APIView(View):
...
@@ -343,6 +356,10 @@ class APIView(View):
neg
=
self
.
perform_content_negotiation
(
request
)
neg
=
self
.
perform_content_negotiation
(
request
)
request
.
accepted_renderer
,
request
.
accepted_media_type
=
neg
request
.
accepted_renderer
,
request
.
accepted_media_type
=
neg
# Determine the API version, if versioning is in use.
version
,
scheme
=
self
.
determine_version
(
request
,
*
args
,
**
kwargs
)
request
.
version
,
request
.
versioning_scheme
=
version
,
scheme
def
finalize_response
(
self
,
request
,
response
,
*
args
,
**
kwargs
):
def
finalize_response
(
self
,
request
,
response
,
*
args
,
**
kwargs
):
"""
"""
Returns the final response object.
Returns the final response object.
...
...
tests/test_versioning.py
0 → 100644
View file @
6e51e4f5
from
django.conf.urls
import
url
from
rest_framework
import
versioning
from
rest_framework.decorators
import
APIView
from
rest_framework.response
import
Response
from
rest_framework.reverse
import
reverse
from
rest_framework.test
import
APIRequestFactory
,
APITestCase
class
RequestVersionView
(
APIView
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
return
Response
({
'version'
:
request
.
version
})
class
ReverseView
(
APIView
):
def
get
(
self
,
request
,
*
args
,
**
kwargs
):
return
Response
({
'url'
:
reverse
(
'another'
,
request
=
request
)})
factory
=
APIRequestFactory
()
mock_view
=
lambda
request
:
None
urlpatterns
=
[
url
(
r'^another/$'
,
mock_view
,
name
=
'another'
)
]
class
TestRequestVersion
:
def
test_unversioned
(
self
):
view
=
RequestVersionView
.
as_view
()
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
None
}
def
test_query_param_versioning
(
self
):
scheme
=
versioning
.
QueryParameterVersioning
view
=
RequestVersionView
.
as_view
(
versioning_class
=
scheme
)
request
=
factory
.
get
(
'/endpoint/?version=1.2.3'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
'1.2.3'
}
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
None
}
def
test_host_name_versioning
(
self
):
scheme
=
versioning
.
HostNameVersioning
view
=
RequestVersionView
.
as_view
(
versioning_class
=
scheme
)
request
=
factory
.
get
(
'/endpoint/'
,
HTTP_HOST
=
'v1.example.org'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
'v1'
}
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
None
}
def
test_accept_header_versioning
(
self
):
scheme
=
versioning
.
AcceptHeaderVersioning
view
=
RequestVersionView
.
as_view
(
versioning_class
=
scheme
)
request
=
factory
.
get
(
'/endpoint/'
,
HTTP_ACCEPT
=
'application/json; version=1.2.3'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
'1.2.3'
}
request
=
factory
.
get
(
'/endpoint/'
,
HTTP_ACCEPT
=
'application/json'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
None
}
def
test_url_path_versioning
(
self
):
scheme
=
versioning
.
URLPathVersioning
view
=
RequestVersionView
.
as_view
(
versioning_class
=
scheme
)
request
=
factory
.
get
(
'/1.2.3/endpoint/'
)
response
=
view
(
request
,
version
=
'1.2.3'
)
assert
response
.
data
==
{
'version'
:
'1.2.3'
}
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'version'
:
None
}
class
TestURLReversing
(
APITestCase
):
urls
=
'tests.test_versioning'
def
test_reverse_unversioned
(
self
):
view
=
ReverseView
.
as_view
()
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'url'
:
'http://testserver/another/'
}
def
test_reverse_host_name_versioning
(
self
):
scheme
=
versioning
.
HostNameVersioning
view
=
ReverseView
.
as_view
(
versioning_class
=
scheme
)
request
=
factory
.
get
(
'/endpoint/'
,
HTTP_HOST
=
'v1.example.org'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'url'
:
'http://v1.example.org/another/'
}
request
=
factory
.
get
(
'/endpoint/'
)
response
=
view
(
request
)
assert
response
.
data
==
{
'url'
:
'http://testserver/another/'
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment