Exemple #1
0
    def _add_oas_resource_definitions(self, resource, path_item):
        """
        add the resource method schema references to the swagger "definitions"
        :param resource:
        :param path_item:
        """
        definitions = {}

        for method in self.get_resource_methods(resource):
            if not method.upper() in HTTP_METHODS:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, "__swagger_operation_object", None)
            if operation:
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)
                if summary:
                    operation["summary"] = summary.split("<br/>")[0]

        try:
            validate_definitions_object(definitions)
        except FRSValidationError:
            safrs.log.critical("Validation failed for {}".format(definitions))
            exit()

        self._swagger_object["definitions"].update(definitions)
Exemple #2
0
    def add_resource(self, resource, *urls, **kwargs):
        """
            This method is partly copied from flask_restful_swagger_2/__init__.py

            I changed it because we don't need path id examples when
            there's no {id} in the path.
            We also have to filter out the unwanted parameters
        """
        #
        # This function has grown out of proportion and should be refactored, disable lint warning for now
        #
        # pylint: disable=too-many-nested-blocks,too-many-statements, too-many-locals
        #
        kwargs.pop("relationship", False)  # relationship object
        SAFRS_INSTANCE_SUFFIX = get_config("OBJECT_ID_SUFFIX") + "}"

        path_item = collections.OrderedDict()
        definitions = {}
        resource_methods = kwargs.get("methods", HTTP_METHODS)
        kwargs.pop("safrs_object", None)
        is_jsonapi_rpc = kwargs.pop(
            "jsonapi_rpc",
            False)  # check if the exposed method is a jsonapi_rpc method
        deprecated = kwargs.pop("deprecated", False)  # TBD!!
        for method in self.get_resource_methods(resource):
            if deprecated:
                continue
            if not method.upper() in resource_methods:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, "__swagger_operation_object", None)
            if operation:
                # operation, definitions_ = self._extract_schemas(operation)
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)
                if summary:
                    operation["summary"] = summary.split("<br/>")[0]

        try:
            validate_definitions_object(definitions)
        except FRSValidationError:
            safrs.log.critical("Validation failed for {}".format(definitions))
            exit()

        self._swagger_object["definitions"].update(definitions)

        if path_item:
            for url in urls:
                if not url.startswith("/"):
                    raise ValidationError("paths must start with a /")
                swagger_url = extract_swagger_path(url)

                # exposing_instance tells us whether we're exposing an instance (as opposed to a collection)
                exposing_instance = swagger_url.strip("/").endswith(
                    SAFRS_INSTANCE_SUFFIX)

                for method in self.get_resource_methods(resource):
                    if method == "post" and exposing_instance:
                        # POSTing to an instance isn't jsonapi-compliant (https://jsonapi.org/format/#crud-creating-client-ids)
                        # "A server MUST return 403 Forbidden in response to an
                        # unsupported request to create a resource with a client-generated ID"
                        # the method has already been added before, remove it & continue
                        path_item.pop(method, None)
                        continue

                    method_doc = copy.deepcopy(path_item.get(method))
                    if not method_doc:
                        continue

                    collection_summary = method_doc.pop(
                        "collection_summary", method_doc.get("summary", None))
                    if not exposing_instance and collection_summary:
                        method_doc["summary"] = collection_summary

                    parameters = []
                    for parameter in method_doc.get("parameters", []):
                        object_id = "{%s}" % parameter.get("name")
                        if method == "get":
                            # Get the jsonapi included resources, ie the exposed relationships
                            param = resource.get_swagger_include()
                            parameters.append(param)

                            # Get the jsonapi fields[], ie the exposed attributes/columns
                            param = resource.get_swagger_fields()
                            parameters.append(param)

                        #
                        # Add the sort, filter parameters to the swagger doc when retrieving a collection
                        #
                        if method == "get" and not (exposing_instance
                                                    or is_jsonapi_rpc):
                            # limit parameter specifies the number of items to return
                            parameters += default_paging_parameters()

                            param = resource.get_swagger_sort()
                            parameters.append(param)

                            parameters += list(resource.get_swagger_filters())

                        if not (parameter.get("in") == "path"
                                and object_id not in swagger_url
                                ) and parameter not in parameters:
                            # Only if a path param is in path url then we add the param
                            parameters.append(parameter)

                    unique_params = OrderedDict()  # rm duplicates
                    for param in parameters:
                        unique_params[param["name"]] = param
                    method_doc["parameters"] = list(unique_params.values())
                    method_doc["operationId"] = self.get_operation_id(
                        path_item.get(method).get("summary", ""))
                    path_item[method] = method_doc

                    instance_schema = method_doc.get("responses",
                                                     {}).get("200", {})
                    if instance_schema and exposing_instance and method_doc[
                            "responses"]["200"].get("schema", None):
                        method_doc["responses"]["200"][
                            "schema"] = resource.SAFRSObject.swagger_models[
                                "instance"].reference()
                        # add this later
                        method_doc["responses"]["200"]["schema"] = {}

                    try:
                        validate_path_item_object(path_item)
                    except FRSValidationError as exc:
                        safrs.log.exception(exc)
                        safrs.log.critical(
                            "Validation failed for {}".format(path_item))
                        exit()

                self._swagger_object["paths"][swagger_url] = path_item
                # Check whether we manage to convert to json
                try:
                    json.dumps(self._swagger_object)
                except Exception:
                    safrs.log.critical("Json encoding failed for")
                    # safrs.log.debug(self._swagger_object)

        # disable API methods that were not set by the SAFRSObject
        for http_method in HTTP_METHODS:
            hm = http_method.lower()
            if hm not in self.get_resource_methods(resource):
                setattr(resource, hm, lambda x:
                        ({}, HTTPStatus.METHOD_NOT_ALLOWED))

        # pylint: disable=bad-super-call
        super(FRSApiBase, self).add_resource(resource, *urls, **kwargs)
Exemple #3
0
    def add_resource(self, resource, *urls, **kwargs):
        '''
            This method is partly copied from flask_restful_swagger_2/__init__.py

            I changed it because we don't need path id examples when
            there's no {id} in the path. We filter out the unwanted parameters
        '''
        SAFRS_INSTANCE_SUFFIX = get_config('OBJECT_ID_SUFFIX') + '}'

        path_item = {}
        definitions = {}
        resource_methods = kwargs.get('methods', HTTP_METHODS)
        safrs_object = kwargs.pop('safrs_object', None)
        for method in [m.lower() for m in resource.methods]:
            if not method.upper() in resource_methods:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, '__swagger_operation_object', None)
            if operation:
                #operation, definitions_ = self._extract_schemas(operation)
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)

                if summary:
                    operation['summary'] = summary.split('<br/>')[0]


        validate_definitions_object(definitions)
        self._swagger_object['definitions'].update(definitions)

        if path_item:
            validate_path_item_object(path_item)
            for url in urls:
                if not url.startswith('/'):
                    raise ValidationError('paths must start with a /')
                swagger_url = extract_swagger_path(url)
                for method in [m.lower() for m in resource.methods]:
                    
                    if method == 'post' and swagger_url.strip('/').endswith(SAFRS_INSTANCE_SUFFIX):
                        # POSTing to an instance isn't jsonapi-compliant (https://jsonapi.org/format/#crud-creating-client-ids)
                        # "A server MUST return 403 Forbidden in response to an
                        # unsupported request to create a resource with a client-generated ID"
                        # the method has already been added before, remove it & continue
                        path_item.pop(method,None)
                        continue
                    
                    method_doc = copy.deepcopy(path_item.get(method))
                    if not method_doc:
                        continue

                    filtered_parameters = []
                    for parameter in method_doc.get('parameters', []):
                        object_id = '{%s}'%parameter.get('name')

                        if method == 'get' and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
                            # limit parameter specifies the number of items to return

                            for param in default_paging_parameters():
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                            param = {'default': ','.join([rel.key for rel in self.safrs_object.__mapper__.relationships]),
                                     'type': 'string',
                                     'name': 'include',
                                     'in': 'query',
                                     'format' : 'string',
                                     'required' : False,
                                     'description' : 'Related relationships to include (csv)'}
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {'default': ','.join(self.safrs_object._s_jsonapi_attrs),
                                     'type': 'string',
                                     'name': 'fields[{}]'.format(self.safrs_object._s_type),
                                     'in': 'query',
                                     'format' : 'string',
                                     'required' : False,
                                     'description' : 'Related relationships to include (csv)'}
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {'default': ','.join(self.safrs_object._s_jsonapi_attrs),
                                     'type': 'string',
                                     'name': 'sort',
                                     'in': 'query',
                                     'format' : 'string',
                                     'required' : False,
                                     'description' : 'Sort order'}
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            for column_name in self.safrs_object._s_column_names:
                                param = {'default': '',
                                         'type': 'string',
                                         'name': 'filter[{}]'.format(column_name),
                                         'in': 'query',
                                         'format' : 'string',
                                         'required' : False,
                                         'description' : '{} attribute filter (csv)'.format(column_name)}
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                        if not (parameter.get('in') == 'path' and not object_id in swagger_url):
                            # Only if a path param is in path url then we add the param
                            filtered_parameters.append(parameter)

                    method_doc['parameters'] = filtered_parameters
                    method_doc['operationId'] = self.get_operation_id(path_item.get(method).get('summary', ''))
                    path_item[method] = method_doc

                    if method == 'get' and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
                        # If no {id} was provided, we return a list of all the objects
                        try:
                            #pylint: disable=invalid-formatstring
                            method_doc['description'] += ' list (See GET /{{} for details)'.format(SAFRS_INSTANCE_SUFFIX)
                            method_doc['responses']['200']['schema'] = ''
                        except:
                            pass

                self._swagger_object['paths'][swagger_url] = path_item

        super(FRSApiBase, self).add_resource(resource, *urls, **kwargs)
Exemple #4
0
    def add_resource(self, resource, *urls, **kwargs):
        '''
            This method is partly copied from flask_restful_swagger_2/__init__.py

            I changed it because we don't need path id examples when 
            there's no {id} in the path. We filter out the unwanted parameters

        '''

        from flask_restful_swagger_2 import validate_definitions_object, parse_method_doc
        from flask_restful_swagger_2 import validate_path_item_object, extract_swagger_path, Extractor

        path_item = {}
        definitions = {}
        resource_methods = kwargs.get(
            'methods', ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'])

        for method in [m.lower() for m in resource.methods]:
            if not method.upper() in resource_methods:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, '__swagger_operation_object', None)
            if operation:
                #operation, definitions_ = self._extract_schemas(operation)
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)
                if summary:
                    operation['summary'] = summary

        validate_definitions_object(definitions)
        self._swagger_object['definitions'].update(definitions)

        if path_item:
            validate_path_item_object(path_item)
            for url in urls:
                if not url.startswith('/'):
                    raise ValidationError('paths must start with a /')
                swagger_url = extract_swagger_path(url)
                for method in [m.lower() for m in resource.methods]:
                    method_doc = copy.deepcopy(path_item.get(method))
                    if not method_doc:
                        continue

                    filtered_parameters = []
                    for parameter in method_doc.get('parameters', []):
                        object_id = '{%s}' % parameter.get('name')

                        if method == 'get' and not swagger_url.endswith(
                                SAFRS_INSTANCE_SUFFIX):
                            # limit parameter specifies the number of items to return
                            param = {
                                'default': 0,  # The 0 isn't rendered though
                                'type': 'integer',
                                'name': 'page[offset]',
                                'in': 'query',
                                'format': 'int64',
                                'required': False,
                                'description': 'Page offset'
                            }
                            if not param in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {
                                'default': 10,
                                'type': 'integer',
                                'name': 'page[limit]',
                                'in': 'query',
                                'format': 'int64',
                                'required': False,
                                'description': 'max number of items'
                            }
                            if not param in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {
                                'default': '',
                                'type': 'string',
                                'name': 'include',
                                'in': 'query',
                                'format': 'int64',
                                'required': False,
                                'description': 'related objects to include'
                            }
                            if not param in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {
                                'default': "",
                                'type': 'string',
                                'name':
                                'fields[{}]'.format(parameter.get('name')),
                                'in': 'query',
                                'format': 'int64',
                                'required': False,
                                'description': 'fields'
                            }
                            if not param in filtered_parameters:
                                filtered_parameters.append(param)

                        if not (parameter.get('in') == 'path'
                                and not object_id in swagger_url):
                            # Only if a path param is in path url then we add the param
                            filtered_parameters.append(parameter)

                    #log.debug(method_doc)
                    method_doc['parameters'] = filtered_parameters
                    path_item[method] = method_doc

                    if method == 'get' and not swagger_url.endswith(
                            SAFRS_INSTANCE_SUFFIX):
                        # If no {id} was provided, we return a list of all the objects
                        try:
                            method_doc[
                                'description'] += ' list (See GET /{{} for details)'.format(
                                    SAFRS_INSTANCE_SUFFIX)
                            method_doc['responses']['200']['schema'] = ''
                        except:
                            pass

                self._swagger_object['paths'][swagger_url] = path_item
        '''self._swagger_object['securityDefinitions'] = {
                "api_key": {
                    "type": "apiKey",
                    "name": "api_key",
                    "in": "query"
                }}

        self._swagger_object['security'] = [ "api_key" ]'''
        super(FRSApiBase, self).add_resource(resource, *urls, **kwargs)
Exemple #5
0
    def add_resource(self, resource, *urls, **kwargs):
        '''
            This method is partly copied from flask_restful_swagger_2/__init__.py

            I changed it because we don't need path id examples when
            there's no {id} in the path. We filter out the unwanted parameters

        '''
        from flask_restful_swagger_2 import validate_definitions_object, parse_method_doc
        from flask_restful_swagger_2 import validate_path_item_object
        from flask_restful_swagger_2 import extract_swagger_path, Extractor
        path_item = {}
        definitions = {}
        resource_methods = kwargs.get('methods', ['GET', 'PUT', 'POST', 'DELETE', 'PATCH'])
        safrs_object = kwargs.get('safrs_object', None)
        if safrs_object:
            del kwargs['safrs_object']
        for method in [m.lower() for m in resource.methods]:
            if not method.upper() in resource_methods:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, '__swagger_operation_object', None)
            if operation:
                #operation, definitions_ = self._extract_schemas(operation)
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)

                if summary:
                    operation['summary'] = summary.split('<br/>')[0]


        validate_definitions_object(definitions)
        self._swagger_object['definitions'].update(definitions)

        if path_item:
            validate_path_item_object(path_item)
            for url in urls:
                if not url.startswith('/'):
                    raise ValidationError('paths must start with a /')
                swagger_url = extract_swagger_path(url)
                for method in [m.lower() for m in resource.methods]:
                    method_doc = copy.deepcopy(path_item.get(method))
                    if not method_doc:
                        continue

                    filtered_parameters = []
                    for parameter in method_doc.get('parameters', []):
                        object_id = '{%s}'%parameter.get('name')

                        if method == 'get' and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
                            # limit parameter specifies the number of items to return

                            for param in default_paging_parameters():
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                            param = {\
                                     'default': ','.join([rel.key for rel in self.safrs_object.__mapper__.relationships]),\
                                     'type': 'string',\
                                     'name': 'include',\
                                     'in': 'query',\
                                     'format' : 'string',\
                                     'required' : False,\
                                     'description' : 'Related relationships to include (csv)'\
                                    }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {'default': "",\
                                     'type': 'string',\
                                     'name': 'fields[{}]'.format(self.safrs_object._s_type),\
                                     'in': 'query',\
                                     'format' : 'int64',\
                                     'required' : False,\
                                     'description' : 'Fields to be selected (csv)'}
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {'default': ','.join(self.safrs_object._s_jsonapi_attrs),\
                                     'type': 'string',\
                                     'name': 'sort',\
                                     'in': 'query',\
                                     'format' : 'string',\
                                     'required' : False,\
                                     'description' : 'Sort order'}
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            for column_name in self.safrs_object._s_column_names:
                                param = {'default': "",\
                                         'type': 'string',\
                                         'name': 'filter[{}]'.format(column_name),\
                                         'in': 'query',\
                                         'format' : 'string',\
                                         'required' : False,\
                                         'description' : '{} attribute filter (csv)'.format(column_name)}
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                        if not (parameter.get('in') == 'path' and not object_id in swagger_url):
                            # Only if a path param is in path url then we add the param
                            filtered_parameters.append(parameter)

                    method_doc['parameters'] = filtered_parameters
                    method_doc['operationId'] = self.get_operation_id(path_item.get(method).get('summary'))
                    path_item[method] = method_doc

                    if method == 'get' and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
                        # If no {id} was provided, we return a list of all the objects
                        try:
                            method_doc['description'] += ' list (See GET /{{} for details)'.\
                                                            format(SAFRS_INSTANCE_SUFFIX)
                            method_doc['responses']['200']['schema'] = ''
                        except:
                            pass

                self._swagger_object['paths'][swagger_url] = path_item

        super(FRSApiBase, self).add_resource(resource, *urls, **kwargs)
Exemple #6
0
    def add_resource(self, resource, *urls, **kwargs):
        """
            This method is partly copied from flask_restful_swagger_2/__init__.py

            I changed it because we don't need path id examples when
            there's no {id} in the path. We filter out the unwanted parameters
        """
        relationship = kwargs.pop("relationship", False)  # relationship object
        SAFRS_INSTANCE_SUFFIX = get_config("OBJECT_ID_SUFFIX") + "}"

        path_item = {}
        definitions = {}
        resource_methods = kwargs.get("methods", HTTP_METHODS)
        kwargs.pop("safrs_object", None)
        is_jsonapi_rpc = kwargs.pop("jsonapi_rpc", False) # check if the exposed method is a jsonapi_rpc method
        for method in [m.lower() for m in resource.methods]:
            if not method.upper() in resource_methods:
                continue
            f = getattr(resource, method, None)
            if not f:
                continue

            operation = getattr(f, "__swagger_operation_object", None)
            if operation:
                # operation, definitions_ = self._extract_schemas(operation)
                operation, definitions_ = Extractor.extract(operation)
                path_item[method] = operation
                definitions.update(definitions_)
                summary = parse_method_doc(f, operation)

                if summary:
                    operation["summary"] = summary.split("<br/>")[0]

        validate_definitions_object(definitions)
        self._swagger_object["definitions"].update(definitions)

        if path_item:
            validate_path_item_object(path_item)
            for url in urls:
                if not url.startswith("/"):
                    raise ValidationError("paths must start with a /")
                swagger_url = extract_swagger_path(url)
                for method in [m.lower() for m in resource.methods]:

                    if method == "post" and swagger_url.strip("/").endswith(SAFRS_INSTANCE_SUFFIX):
                        # POSTing to an instance isn't jsonapi-compliant (https://jsonapi.org/format/#crud-creating-client-ids)
                        # "A server MUST return 403 Forbidden in response to an
                        # unsupported request to create a resource with a client-generated ID"
                        # the method has already been added before, remove it & continue
                        path_item.pop(method, None)
                        continue

                    method_doc = copy.deepcopy(path_item.get(method))
                    if not method_doc:
                        continue

                    filtered_parameters = []
                    for parameter in method_doc.get("parameters", []):
                        object_id = "{%s}" % parameter.get("name")

                        if method == "get" and relationship:
                            default_include = ",".join(
                                [rel.key for rel in relationship.mapper.class_.__mapper__.relationships]
                            )
                            param = {
                                "default": default_include,
                                "type": "string",
                                "name": "include",
                                "in": "query",
                                "format": "string",
                                "required": False,
                                "description": "Related relationships to include (csv)",
                            }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                        if method == "get" and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX) and not is_jsonapi_rpc:
                            # limit parameter specifies the number of items to return

                            for param in default_paging_parameters():
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                            default_include = ",".join([rel.key for rel in self.safrs_object.__mapper__.relationships])

                            param = {
                                "default": default_include,
                                "type": "string",
                                "name": "include",
                                "in": "query",
                                "format": "string",
                                "required": False,
                                "description": "Related relationships to include (csv)",
                            }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            param = {
                                "default": ",".join(self.safrs_object._s_jsonapi_attrs),
                                "type": "string",
                                "name": "fields[{}]".format(self.safrs_object._s_type),
                                "in": "query",
                                "format": "string",
                                "required": False,
                                "description": "Related relationships to include (csv)",
                            }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            sort_attrs = self.safrs_object._s_jsonapi_attrs

                            param = {
                                "default": ",".join(sort_attrs),
                                "type": "string",
                                "name": "sort",
                                "in": "query",
                                "format": "string",
                                "required": False,
                                "description": "Sort order",
                            }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                            for column_name in self.safrs_object._s_column_names:
                                param = {
                                    "default": "",
                                    "type": "string",
                                    "name": "filter[{}]".format(column_name),
                                    "in": "query",
                                    "format": "string",
                                    "required": False,
                                    "description": "{} attribute filter (csv)".format(column_name),
                                }
                                if param not in filtered_parameters:
                                    filtered_parameters.append(param)

                            param = {
                                "default": "",
                                "type": "string",
                                "name": "filter",
                                "in": "query",
                                "format": "string",
                                "required": False,
                                "description": "Custom filter",
                            }
                            if param not in filtered_parameters:
                                filtered_parameters.append(param)

                        if not (parameter.get("in") == "path" and not object_id in swagger_url) and parameter not in filtered_parameters:
                            # Only if a path param is in path url then we add the param
                            filtered_parameters.append(parameter)

                    method_doc["parameters"] = filtered_parameters
                    method_doc["operationId"] = self.get_operation_id(path_item.get(method).get("summary", ""))
                    path_item[method] = method_doc

                    if method == "get" and not swagger_url.endswith(SAFRS_INSTANCE_SUFFIX):
                        # If no {id} was provided, we return a list of all the objects
                        # pylint: disable=invalid-formatstring
                        try:
                            method_doc["description"] += " list (See GET /{{} for details)".format(
                                SAFRS_INSTANCE_SUFFIX
                            )
                            method_doc["responses"]["200"]["schema"] = ""
                        except:
                            pass

                self._swagger_object["paths"][swagger_url] = path_item

        super(FRSApiBase, self).add_resource(resource, *urls, **kwargs)