diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/interfaces/api.py | 37 | ||||
| -rw-r--r-- | src/mailman/rest/addresses.py | 3 | ||||
| -rw-r--r-- | src/mailman/rest/api.py | 54 | ||||
| -rw-r--r-- | src/mailman/rest/helpers.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/lists.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/members.py | 38 | ||||
| -rw-r--r-- | src/mailman/rest/post_moderation.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/preferences.py | 2 | ||||
| -rw-r--r-- | src/mailman/rest/root.py | 29 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_addresses.py | 4 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_validator.py | 15 | ||||
| -rw-r--r-- | src/mailman/rest/users.py | 54 | ||||
| -rw-r--r-- | src/mailman/rest/validator.py | 12 | ||||
| -rw-r--r-- | src/mailman/rest/wsgiapp.py | 4 |
14 files changed, 158 insertions, 100 deletions
diff --git a/src/mailman/interfaces/api.py b/src/mailman/interfaces/api.py new file mode 100644 index 000000000..f2b33ea09 --- /dev/null +++ b/src/mailman/interfaces/api.py @@ -0,0 +1,37 @@ +# Copyright (C) 2016 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""REST web service API context.""" + +__all__ = [ + 'IAPI', + ] + + +from zope.interface import Attribute, Interface + + +class IAPI(Interface): + """The REST web service context.""" + + version = Attribute("""The REST API version.""") + + def from_uuid(uuid): + """Return the string representation of a UUID.""" + + def to_uuid(uuid_repr): + """Return the UUID from the string representation.""" diff --git a/src/mailman/rest/addresses.py b/src/mailman/rest/addresses.py index 10edb3741..83ff87454 100644 --- a/src/mailman/rest/addresses.py +++ b/src/mailman/rest/addresses.py @@ -59,8 +59,7 @@ class _AddressBase(CollectionMixin): if address.verified_on: representation['verified_on'] = address.verified_on if address.user: - uid = getattr(address.user.user_id, - 'int' if self.api_version == '3.0' else 'hex') + uid = self.api.from_uuid(address.user.user_id) representation['user'] = self.path_to('users/{}'.format(uid)) return representation diff --git a/src/mailman/rest/api.py b/src/mailman/rest/api.py new file mode 100644 index 000000000..998fac268 --- /dev/null +++ b/src/mailman/rest/api.py @@ -0,0 +1,54 @@ +# Copyright (C) 2016 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""REST web service API contexts.""" + +__all__ = [ + 'API30', + 'API31', + ] + + +from mailman.interfaces.api import IAPI +from uuid import UUID +from zope.interface import implementer + + +@implementer(IAPI) +class API30: + version = '3.0' + + @staticmethod + def from_uuid(uuid): + return uuid.int + + @staticmethod + def to_uuid(uuid_repr): + return UUID(int=int(uuid_repr)) + + +@implementer(IAPI) +class API31: + version = '3.1' + + @staticmethod + def from_uuid(uuid): + return uuid.hex + + @staticmethod + def to_uuid(uuid_repr): + return UUID(hex=uuid_repr) diff --git a/src/mailman/rest/helpers.py b/src/mailman/rest/helpers.py index 6a1408988..425164ef7 100644 --- a/src/mailman/rest/helpers.py +++ b/src/mailman/rest/helpers.py @@ -186,7 +186,7 @@ class CollectionMixin: return result def path_to(self, resource): - return path_to(resource, self.api_version) + return path_to(resource, self.api.version) diff --git a/src/mailman/rest/lists.py b/src/mailman/rest/lists.py index a2b9b4a9c..426c9c633 100644 --- a/src/mailman/rest/lists.py +++ b/src/mailman/rest/lists.py @@ -155,7 +155,7 @@ class AList(_ListBase): email, self._mlist.list_id, role) if member is None: return NotFound(), [] - return AMember(request.context['api_version'], member.member_id) + return AMember(request.context['api'], member.member_id) @child(roster_matcher) def roster(self, request, segments, role): diff --git a/src/mailman/rest/members.py b/src/mailman/rest/members.py index bdaf031ab..b666bc2f4 100644 --- a/src/mailman/rest/members.py +++ b/src/mailman/rest/members.py @@ -27,7 +27,7 @@ __all__ = [ from mailman.app.membership import add_member, delete_member from mailman.interfaces.action import Action -from mailman.interfaces.address import IAddress, InvalidEmailAddressError +from mailman.interfaces.address import IAddress from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, MemberRole, MembershipError, @@ -43,7 +43,6 @@ from mailman.rest.helpers import ( from mailman.rest.preferences import Preferences, ReadOnlyPreferences from mailman.rest.validator import ( Validator, enum_validator, subscriber_validator) -from operator import attrgetter from uuid import UUID from zope.component import getUtility @@ -52,10 +51,6 @@ from zope.component import getUtility class _MemberBase(CollectionMixin): """Shared base class for member representations.""" - def _get_uuid(self, member): - return getattr(member.member_id, - 'int' if self.api_version == '3.0' else 'hex') - def _resource_as_dict(self, member): """See `CollectionMixin`.""" enum, dot, role = str(member.role).partition('.') @@ -66,7 +61,7 @@ class _MemberBase(CollectionMixin): # member_id are UUIDs. In API 3.0 we use the integer equivalent of # the UID in the URL, but in API 3.1 we use the hex equivalent. See # issue #121 for details. - member_id = self._get_uuid(member) + member_id = self.api.from_uuid(member.member_id) response = dict( address=self.path_to('addresses/{}'.format(member.address.email)), delivery_mode=member.delivery_mode, @@ -80,8 +75,7 @@ class _MemberBase(CollectionMixin): # Add the user link if there is one. user = member.user if user is not None: - user_id = getattr(user.user_id, - 'int' if self.api_version == '3.0' else 'hex') + user_id = self.api.from_uuid(user.user_id) response['user'] = self.path_to('users/{}'.format(user_id)) return response @@ -112,16 +106,13 @@ class MemberCollection(_MemberBase): class AMember(_MemberBase): """A member.""" - def __init__(self, api_version, member_id_string): + def __init__(self, api, member_id_string): # The member_id_string is the string representation of the member's # UUID. In API 3.0, the argument is the string representation of the # int representation of the UUID. In API 3.1 it's the hex. - self.api_version = api_version + self.api = api try: - if api_version == '3.0': - member_id = UUID(int=int(member_id_string)) - else: - member_id = UUID(hex=member_id_string) + member_id = api.to_uuid(member_id_string) except ValueError: # The string argument could not be converted to a UUID. self._member = None @@ -143,7 +134,7 @@ class AMember(_MemberBase): return NotFound(), [] if self._member is None: return NotFound(), [] - member_id = self._get_uuid(self._member) + member_id = self.api.from_uuid(self._member.member_id) child = Preferences( self._member.preferences, 'members/{}'.format(member_id)) return child, [] @@ -157,7 +148,8 @@ class AMember(_MemberBase): return NotFound(), [] child = ReadOnlyPreferences( self._member, - 'members/{}/all'.format(self._get_uuid(self._member))) + 'members/{}/all'.format( + self.api.from_uuid(self._member.member_id))) return child, [] def on_delete(self, request, response): @@ -220,7 +212,7 @@ class AllMembers(_MemberBase): try: validator = Validator( list_id=str, - subscriber=subscriber_validator(self.api_version), + subscriber=subscriber_validator(self.api), display_name=str, delivery_mode=enum_validator(DeliveryMode), role=enum_validator(MemberRole), @@ -292,7 +284,7 @@ class AllMembers(_MemberBase): # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. - member_id = self._get_uuid(member) + member_id = self.api.from_uuid(member.member_id) location = self.path_to('members/{}'.format(member_id)) created(response, location) return @@ -339,7 +331,7 @@ class AllMembers(_MemberBase): # and return the location to the new member. Member ids are # UUIDs and need to be converted to URLs because JSON doesn't # directly support UUIDs. - member_id = self._get_uuid(member) + member_id = self.api.from_uuid(member.member_id) location = self.path_to('members/{}'.format(member_id)) created(response, location) @@ -353,10 +345,10 @@ class AllMembers(_MemberBase): class _FoundMembers(MemberCollection): """The found members collection.""" - def __init__(self, members, api_version): + def __init__(self, members, api): super().__init__() self._members = members - self.api_version = api_version + self.api = api def _get_collection(self, request): """See `CollectionMixin`.""" @@ -380,5 +372,5 @@ class FindMembers(_MemberBase): bad_request(response, str(error)) else: members = service.find_members(**data) - resource = _FoundMembers(members, self.api_version) + resource = _FoundMembers(members, self.api) okay(response, etag(resource._make_collection(request))) diff --git a/src/mailman/rest/post_moderation.py b/src/mailman/rest/post_moderation.py index 673ff68e8..a9b3ba1b7 100644 --- a/src/mailman/rest/post_moderation.py +++ b/src/mailman/rest/post_moderation.py @@ -64,7 +64,7 @@ class _ModerationBase: # Add a self_link. resource['self_link'] = path_to( 'lists/{}/held/{}'.format(self._mlist.list_id, request_id), - self.api_version) + self.api.version) return resource diff --git a/src/mailman/rest/preferences.py b/src/mailman/rest/preferences.py index 032330c7f..4299dc0cd 100644 --- a/src/mailman/rest/preferences.py +++ b/src/mailman/rest/preferences.py @@ -66,7 +66,7 @@ class ReadOnlyPreferences: # Add the self link. resource['self_link'] = path_to( '{}/preferences'.format(self._base_url), - self.api_version) + self.api.version) okay(response, etag(resource)) diff --git a/src/mailman/rest/root.py b/src/mailman/rest/root.py index b97ef7df7..8e6e9f716 100644 --- a/src/mailman/rest/root.py +++ b/src/mailman/rest/root.py @@ -31,6 +31,7 @@ from mailman.core.system import system from mailman.interfaces.listmanager import IListManager from mailman.model.uid import UID from mailman.rest.addresses import AllAddresses, AnAddress +from mailman.rest.api import API30, API31 from mailman.rest.domains import ADomain, AllDomains from mailman.rest.helpers import ( BadRequest, NotFound, child, etag, no_content, not_found, okay, path_to) @@ -59,7 +60,7 @@ class Root: @child('3.0') def api_version_30(self, request, segments): # API version 3.0 was introduced in Mailman 3.0. - request.context['api_version'] = '3.0' + request.context['api'] = API30 return self._check_authorization(request, segments) @child('3.1') @@ -68,7 +69,7 @@ class Root: # incompatible difference is that uuids are represented as hex strings # instead of 128 bit integers. The latter is not compatible with all # versions of JavaScript. - request.context['api_version'] = '3.1' + request.context['api'] = API31 return self._check_authorization(request, segments) def _check_authorization(self, request, segments): @@ -102,8 +103,8 @@ class Versions: resource = dict( mailman_version=system.mailman_version, python_version=system.python_version, - api_version=self.api_version, - self_link=path_to('system/versions', self.api_version), + api_version=self.api.version, + self_link=path_to('system/versions', self.api.version), ) okay(response, etag(resource)) @@ -179,12 +180,12 @@ class TopLevel: """ if len(segments) == 0: resource = AllAddresses() - resource.api_version = request.context['api_version'] + resource.api = request.context['api'] return resource else: email = segments.pop(0) resource = AnAddress(email) - resource.api_version = request.context['api_version'] + resource.api = request.context['api'] return resource, segments @child() @@ -218,32 +219,32 @@ class TopLevel: @child() def members(self, request, segments): """/<api>/members""" - api_version = request.context['api_version'] + api = request.context['api'] if len(segments) == 0: resource = AllMembers() - resource.api_version = api_version + resource.api = api return resource # Either the next segment is the string "find" or a member id. They # cannot collide. segment = segments.pop(0) if segment == 'find': resource = FindMembers() - resource.api_version = api_version + resource.api = api else: - resource = AMember(api_version, segment) + resource = AMember(api, segment) return resource, segments @child() def users(self, request, segments): """/<api>/users""" - api_version = request.context['api_version'] + api = request.context['api'] if len(segments) == 0: resource = AllUsers() - resource.api_version = api_version + resource.api = api return resource else: user_id = segments.pop(0) - return AUser(api_version, user_id), segments + return AUser(api, user_id), segments @child() def owners(self, request, segments): @@ -252,7 +253,7 @@ class TopLevel: return BadRequest(), [] else: resource = ServerOwners() - resource.api_version = request.context['api_version'] + resource.api = request.context['api'] return resource, segments @child() diff --git a/src/mailman/rest/tests/test_addresses.py b/src/mailman/rest/tests/test_addresses.py index d2a508225..bbb6ee2d1 100644 --- a/src/mailman/rest/tests/test_addresses.py +++ b/src/mailman/rest/tests/test_addresses.py @@ -542,7 +542,7 @@ class TestAPI31Addresses(unittest.TestCase): }) self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, - b'badly formed hexadecimal UUID string') + b'Cannot convert parameters: user_id') def test_user_subresource_put(self): # By PUTing to the 'user' resource, you can change the user that an @@ -577,4 +577,4 @@ class TestAPI31Addresses(unittest.TestCase): }, method='PUT') self.assertEqual(cm.exception.code, 400) self.assertEqual(cm.exception.reason, - b'badly formed hexadecimal UUID string') + b'Cannot convert parameters: user_id') diff --git a/src/mailman/rest/tests/test_validator.py b/src/mailman/rest/tests/test_validator.py index 267372b41..7d7900aaf 100644 --- a/src/mailman/rest/tests/test_validator.py +++ b/src/mailman/rest/tests/test_validator.py @@ -25,6 +25,7 @@ __all__ = [ import unittest from mailman.interfaces.usermanager import IUserManager +from mailman.rest.api import API30, API31 from mailman.rest.validator import ( list_of_strings_validator, subscriber_validator) from mailman.testing.layers import RESTLayer @@ -53,35 +54,35 @@ class TestValidators(unittest.TestCase): def test_subscriber_validator_int_uuid(self): # Convert from an existing user id to a UUID. anne = getUtility(IUserManager).make_user('anne@example.com') - uuid = subscriber_validator('3.0')(str(anne.user_id.int)) + uuid = subscriber_validator(API30)(str(anne.user_id.int)) self.assertEqual(anne.user_id, uuid) def test_subscriber_validator_hex_uuid(self): # Convert from an existing user id to a UUID. anne = getUtility(IUserManager).make_user('anne@example.com') - uuid = subscriber_validator('3.1')(anne.user_id.hex) + uuid = subscriber_validator(API31)(anne.user_id.hex) self.assertEqual(anne.user_id, uuid) def test_subscriber_validator_no_int_uuid(self): # API 3.1 does not accept ints as subscriber id's. anne = getUtility(IUserManager).make_user('anne@example.com') self.assertRaises(ValueError, - subscriber_validator('3.1'), str(anne.user_id.int)) + subscriber_validator(API31), str(anne.user_id.int)) def test_subscriber_validator_bad_int_uuid(self): # In API 3.0, UUIDs are ints. self.assertRaises(ValueError, - subscriber_validator('3.0'), 'not-a-thing') + subscriber_validator(API30), 'not-a-thing') def test_subscriber_validator_bad_int_hex(self): # In API 3.1, UUIDs are hexes. self.assertRaises(ValueError, - subscriber_validator('3.1'), 'not-a-thing') + subscriber_validator(API31), 'not-a-thing') def test_subscriber_validator_email_address_API30(self): - self.assertEqual(subscriber_validator('3.0')('anne@example.com'), + self.assertEqual(subscriber_validator(API30)('anne@example.com'), 'anne@example.com') def test_subscriber_validator_email_address_API31(self): - self.assertEqual(subscriber_validator('3.1')('anne@example.com'), + self.assertEqual(subscriber_validator(API31)('anne@example.com'), 'anne@example.com') diff --git a/src/mailman/rest/users.py b/src/mailman/rest/users.py index 6e86c395b..017a09525 100644 --- a/src/mailman/rest/users.py +++ b/src/mailman/rest/users.py @@ -40,7 +40,6 @@ from mailman.rest.preferences import Preferences from mailman.rest.validator import ( PatchValidator, Validator, list_of_strings_validator) from passlib.utils import generate_password as generate -from uuid import UUID from zope.component import getUtility @@ -117,9 +116,9 @@ def create_user(arguments, request, response): password = generate(int(config.passwords.password_length)) user.password = config.password_context.encrypt(password) user.is_server_owner = is_server_owner - api_version = request.context['api_version'] - user_id = getattr(user.user_id, 'int' if api_version == '3.0' else 'hex') - location = path_to('users/{}'.format(user_id), api_version) + api = request.context['api'] + user_id = api.from_uuid(user.user_id) + location = path_to('users/{}'.format(user_id), api.version) created(response, location) return user @@ -128,17 +127,13 @@ def create_user(arguments, request, response): class _UserBase(CollectionMixin): """Shared base class for user representations.""" - def _get_uuid(self, user): - return getattr(user.user_id, - 'int' if self.api_version == '3.0' else 'hex') - def _resource_as_dict(self, user): """See `CollectionMixin`.""" # The canonical URL for a user is their unique user id, although we # can always look up a user based on any registered and validated # email address associated with their account. The user id is a UUID, # but we serialize its integer equivalent. - user_id = self._get_uuid(user) + user_id = self.api.from_uuid(user.user_id) resource = dict( created_on=user.created_on, is_server_owner=user.is_server_owner, @@ -182,9 +177,11 @@ class AllUsers(_UserBase): class AUser(_UserBase): """A user.""" - def __init__(self, api_version, user_identifier): + def __init__(self, api, user_identifier): """Get a user by various type of identifiers. + :param api: The REST API object. + :type api: IAPI :param user_identifier: The identifier used to retrieve the user. The identifier may either be an email address controlled by the user or the UUID of the user. The type of identifier is auto-detected @@ -193,7 +190,7 @@ class AUser(_UserBase): API 3.0 are integers, while in 3.1 are hex. :type user_identifier: string """ - self.api_version = api_version + self.api = api user_manager = getUtility(IUserManager) if '@' in user_identifier: self._user = user_manager.get_user(user_identifier) @@ -201,10 +198,7 @@ class AUser(_UserBase): # The identifier is the string representation of a UUID, either an # int in API 3.0 or a hex in API 3.1. try: - if api_version == '3.0': - user_id = UUID(int=int(user_identifier)) - else: - user_id = UUID(hex=user_identifier) + user_id = api.to_uuid(user_identifier) except ValueError: self._user = None else: @@ -244,7 +238,7 @@ class AUser(_UserBase): return NotFound(), [] child = Preferences( self._user.preferences, - 'users/{}'.format(self._get_uuid(self._user))) + 'users/{}'.format(self.api.from_uuid(self._user.user_id))) return child, [] def on_patch(self, request, response): @@ -319,14 +313,14 @@ class AddressUser(_UserBase): if self._user: conflict(response) return - api_version = request.context['api_version'] + api = request.context['api'] # When creating a linked user by POSTing, the user either must already # exist, or it can be automatically created, if the auto_create flag # is given and true (if missing, it defaults to true). However, in # this case we do not accept 'email' as a POST field. fields = CREATION_FIELDS.copy() del fields['email'] - fields['user_id'] = (int if api_version == '3.0' else str) + fields['user_id'] = api.to_uuid fields['auto_create'] = as_boolean fields['_optional'] = fields['_optional'] + ( 'user_id', 'auto_create', 'is_server_owner') @@ -338,16 +332,10 @@ class AddressUser(_UserBase): return user_manager = getUtility(IUserManager) if 'user_id' in arguments: - raw_uid = arguments['user_id'] - kws = {('int' if api_version == '3.0' else 'hex'): raw_uid} - try: - user_id = UUID(**kws) - except ValueError as error: - bad_request(response, str(error)) - return + user_id = arguments['user_id'] user = user_manager.get_user_by_id(user_id) if user is None: - not_found(response, b'No user with ID {}'.format(raw_uid)) + not_found(response, b'No user with ID {}'.format(user_id)) return okay(response) else: @@ -364,12 +352,12 @@ class AddressUser(_UserBase): def on_put(self, request, response): """Set or replace the addresses's user.""" - api_version = request.context['api_version'] + api = request.context['api'] if self._user: self._user.unlink(self._address) # Process post data and check for an existing user. fields = CREATION_FIELDS.copy() - fields['user_id'] = (int if api_version == '3.0' else str) + fields['user_id'] = api.to_uuid fields['_optional'] = fields['_optional'] + ( 'user_id', 'email', 'is_server_owner') try: @@ -380,16 +368,10 @@ class AddressUser(_UserBase): return user_manager = getUtility(IUserManager) if 'user_id' in arguments: - raw_uid = arguments['user_id'] - kws = {('int' if api_version == '3.0' else 'hex'): raw_uid} - try: - user_id = UUID(**kws) - except ValueError as error: - bad_request(response, str(error)) - return + user_id = arguments['user_id'] user = user_manager.get_user_by_id(user_id) if user is None: - not_found(response, b'No user with ID {}'.format(raw_uid)) + not_found(response, b'No user with ID {}'.format(user_id)) return okay(response) else: diff --git a/src/mailman/rest/validator.py b/src/mailman/rest/validator.py index a20ec0ce5..35be36be4 100644 --- a/src/mailman/rest/validator.py +++ b/src/mailman/rest/validator.py @@ -31,7 +31,6 @@ from mailman.core.errors import ( ReadOnlyPATCHRequestError, UnknownPATCHRequestError) from mailman.interfaces.address import IEmailValidator from mailman.interfaces.languages import ILanguageManager -from uuid import UUID from zope.component import getUtility @@ -55,18 +54,11 @@ class enum_validator: raise ValueError(exception.args[0]) -def subscriber_validator(api_version): +def subscriber_validator(api): """Convert an email-or-(int|hex) to an email-or-UUID.""" def _inner(subscriber): - # In API 3.0, the uuid is represented by an int, so if we can int - # convert the value, we know it's a UUID-as-int. In API 3.1 though, - # uuids are represented by the hex version, which of course cannot - # include an @ sign. try: - if api_version == '3.0': - return UUID(int=int(subscriber)) - else: - return UUID(hex=subscriber) + return api.to_uuid(subscriber) except ValueError: # It must be an email address. if getUtility(IEmailValidator).is_valid(subscriber): diff --git a/src/mailman/rest/wsgiapp.py b/src/mailman/rest/wsgiapp.py index 9bac53c71..3155ee510 100644 --- a/src/mailman/rest/wsgiapp.py +++ b/src/mailman/rest/wsgiapp.py @@ -76,7 +76,7 @@ class AdminWebServiceWSGIRequestHandler(WSGIRequestHandler): class SetAPIVersion: - """Falcon middleware object that sets the api_version on resources.""" + """Falcon middleware object that sets the API on resources.""" def process_resource(self, request, response, resource): # Set this attribute on the resource right before it is dispatched @@ -88,7 +88,7 @@ class SetAPIVersion: # resource path does not exist. This middleware method will still get # called, but there's nothing to set the api_version on. if resource is not None: - resource.api_version = request.context.get('api_version') + resource.api = request.context.get('api') class RootedAPI(API): |
