コード例 #1
0
ファイル: jsonapi.py プロジェクト: westlifeljz/safrs
    def patch(self, **kwargs):
        """
        summary : Update {class_name}
        description: Update {class_name} attributes
        responses:
            200 :
                description : Request fulfilled, document follows
            202 :
                description : Accepted
            204 :
                description : No Content
            403:
                description : Forbidden
            404 :
                description : Not Found
            409 :
                description : Conflict
        ---
        https://jsonapi.org/format/#crud-updating-responses
        Update the object with the specified id
        """
        id = kwargs.get(self._s_object_id, None)

        payload = request.get_jsonapi_payload()
        if not isinstance(payload, dict):
            raise ValidationError("Invalid Object Type")

        data = payload.get("data")
        if id is None and isinstance(data, list):
            # Bulk patch request
            for item in data:
                if not isinstance(item, dict):
                    raise ValidationError("Invalid Data Object")
                instance = self._patch_instance(item)
            response = make_response(jsonify({}), HTTPStatus.ACCEPTED)

        elif not data or not isinstance(data, dict):
            raise ValidationError("Invalid Data Object")
        elif id is None:
            raise ValidationError("Invalid ID")
        else:
            instance = self._patch_instance(data, id)
            # object id is the endpoint parameter, for example "UserId" for a User SAFRSObject
            obj_args = {instance._s_object_id: instance.jsonapi_id}
            # Retrieve the jsonapi encoded object and return it to the client
            obj_data = self.get(**obj_args)
            response = make_response(obj_data, HTTPStatus.OK)
            # Set the Location header to the newly created object
            response.headers["Location"] = url_for(self.endpoint, **obj_args)

        return response
コード例 #2
0
    def post(self, **kwargs):
        """
        summary : call
        responses :
            403:
                description :
            201:
                description: Created
            202:
                description : Accepted
            403 :
                description : Forbidden
            404:
                description : Not Found
            409:
                description : Conflict
        ---
        HTTP POST: apply actions, return 200 regardless.
        The actual jsonapi_rpc method may return other codes
        """
        id = kwargs.get(self._s_object_id, None)

        if id is not None:
            instance = self.SAFRSObject.get_instance(id)
            if not instance:
                # If no instance was found this means the user supplied
                # an invalid ID
                raise ValidationError("Invalid ID")
        else:
            # No ID was supplied, apply method to the class itself
            instance = self.SAFRSObject

        method = getattr(instance, self.method_name, None)

        if not method:
            # Only call methods for Campaign and not for superclasses (e.g. safrs.DB.Model)
            raise ValidationError('Invalid method "{}"'.format(
                self.method_name))
        if not is_public(method):
            raise ValidationError("Method is not public")

        args = dict(request.args)
        if getattr(method, "valid_jsonapi", False):
            payload = request.get_jsonapi_payload()
            if payload:
                args = payload.get("meta", {}).get("args", {})
        else:
            args = request.get_json()

        return self._create_rpc_response(method, args)
コード例 #3
0
ファイル: jsonapi.py プロジェクト: macleodbroad-wf/safrs
    def patch(self, **kwargs):
        """
            responses:
                200 :
                    description : Accepted
                201 :
                    description: Created
                204 :
                    description : No Content
                403:
                    description : Forbidden
                404 :
                    description : Not Found
                409 :
                    description : Conflict
            ----
            Update or create a relationship child item
            to be used to create or update one-to-many mappings
            but also works for many-to-many etc.

            # Updating To-One Relationships

            http://jsonapi.org/format/#crud-updating-to-one-relationships:
            A server MUST respond to PATCH requests to a URL
            from a to-one relationship link as described below

            The PATCH request MUST include a top-level member named data containing one of:
            a resource identifier object corresponding to the new related resource.
            null, to remove the relationship.
        """
        parent, relation = self.parse_args(**kwargs)
        json_reponse = request.get_jsonapi_payload()
        if not isinstance(json_reponse, dict):
            raise ValidationError("Invalid Object Type")

        data = json_reponse.get("data")
        relation = getattr(parent, self.rel_name)
        obj_args = {self.parent_object_id: parent.jsonapi_id}

        if isinstance(data, dict):
            # => Update TOONE Relationship
            # TODO!!!
            if self.SAFRSObject.relationship.direction != MANYTOONE:
                raise GenericError(
                    "To PATCH a TOMANY relationship you should provide a list")
            child = self.child_class.get_instance(data.get("id", None))
            setattr(parent, self.rel_name, child)
            obj_args[self.child_object_id] = child.jsonapi_id

        elif isinstance(data, list):
            """
                http://jsonapi.org/format/#crud-updating-to-many-relationships

                If a client makes a PATCH request to a URL from a to-many relationship link,
                the server MUST either completely replace every member of the relationship,
                return an appropriate error response if some resourcescan not be found
                or accessed, or return a 403 Forbidden response if complete
                replacement is not allowed by the server.
            """
            if self.SAFRSObject.relationship.direction == MANYTOONE:
                raise GenericError("To PATCH a MANYTOONE relationship you \
                should provide a dictionary instead of a list")
            # first remove all items, then append the new items
            # if the relationship has been configured with lazy="dynamic"
            # then it is a subclass of AppenderBaseQuery and
            # we should empty the relationship by setting it to []
            # otherwise it is an instance of InstrumentedList and we have to empty it
            # ( we could loop all items but this is slower for large collections )
            tmp_rel = []
            for child in data:
                if not isinstance(child, dict):
                    raise ValidationError("Invalid data object")
                child_instance = self.child_class.get_instance(child["id"])
                tmp_rel.append(child_instance)

            if isinstance(relation,
                          sqlalchemy.orm.collections.InstrumentedList):
                relation[:] = tmp_rel
            else:
                setattr(parent, self.rel_name, tmp_rel)

        elif data is None:
            # { data : null } //=> clear the relationship
            if self.SAFRSObject.relationship.direction == MANYTOONE:
                child = getattr(parent, self.SAFRSObject.relationship.key)
                if child:
                    pass
                setattr(parent, self.rel_name, None)
            else:
                #
                # should we allow this??
                # maybe we just want to raise an error here ...???
                setattr(parent, self.rel_name, [])
        else:
            raise ValidationError("Invalid Data Object Type")

        if data is None:
            # item removed from relationship => 202 accepted
            # TODO: add response to swagger
            # add meta?

            response = {}, 200
        else:
            obj_data = self.get(**obj_args)
            # Retrieve the object json and return it to the client
            response = make_response(obj_data, 201)
            # Set the Location header to the newly created object
            # todo: set location header
            # response.headers['Location'] = url_for(self.endpoint, **obj_args)
        return response
コード例 #4
0
ファイル: jsonapi.py プロジェクト: macleodbroad-wf/safrs
    def post(self, **kwargs):
        """
            responses :
                403:
                    description : This implementation does not accept client-generated IDs
                201:
                    description: Created
                202:
                    description : Accepted
                404:
                    description : Not Found
                409:
                    description : Conflict
            ---
            http://jsonapi.org/format/#crud-creating
            Creating Resources
            A resource can be created by sending a POST request to a URL
            that represents a collection of resources.
            The request MUST include a single resource object as primary data.
            The resource object MUST contain at least a type member.

            If a relationship is provided in the relationships member of the resource object,
            its value MUST be a relationship object with a data member.
            The value of this key represents the linkage the new resource is to have.

            Response:
            403: This implementation does not accept client-generated IDs
            201: Created
            202: Accepted
            404: Not Found
            409: Conflict

            Location Header identifying the location of the newly created resource
            Body : created object

            TODO:
            409 Conflict
              A server MUST return 409 Conflict when processing a POST request
              to create a resource with a client-generated ID that already exists.
              A server MUST return 409 Conflict when processing a POST request
              in which the resource object’s type is not among the type(s) that
              constitute the collection represented by the endpoint.
              A server SHOULD include error details and provide enough
              information to recognize the source of the conflict.
        """
        payload = request.get_jsonapi_payload()
        method_name = payload.get("meta", {}).get("method", None)

        id = kwargs.get(self.object_id, None)
        if id is not None:
            # 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"
            response = {"meta": {"error": "Unsupported JSONAPI Request"}}, 403
            return response

        else:
            # Create a new instance of the SAFRSObject
            data = payload.get("data")
            if data is None:
                raise ValidationError("Request contains no data")
            if not isinstance(data, dict):
                raise ValidationError("data is not a dict object")

            obj_type = data.get("type", None)
            if not obj_type:  # or type..
                raise ValidationError("Invalid type member")

            attributes = data.get("attributes", {})
            # Remove 'id' (or other primary keys) from the attributes, unless it is allowed by the
            # SAFRSObject allow_client_generated_ids attribute
            for col_name in [c.name for c in self.SAFRSObject.id_type.columns]:
                attributes.pop(col_name, None)

            # remove attributes that have relationship names
            attributes = {
                attr_name: attributes[attr_name]
                for attr_name in attributes
                if attr_name not in self.SAFRSObject._s_relationship_names
            }

            if getattr(self.SAFRSObject, "allow_client_generated_ids",
                       False) is True:
                # todo, this isn't required per the jsonapi spec, doesn't work well and isn't documented, maybe later
                id = data.get("id")
                self.SAFRSObject.id_type.get_pks(id)

            # Create the object instance with the specified id and json data
            # If the instance (id) already exists, it will be updated with the data
            # pylint: disable=not-callable
            instance = self.SAFRSObject(**attributes)

            if not instance.db_commit:
                #
                # The item has not yet been added/commited by the SAFRSBase,
                # in that case we have to do it ourselves
                #
                safrs.DB.session.add(instance)
                try:
                    safrs.DB.session.commit()
                except sqlalchemy.exc.SQLAlchemyError as exc:
                    # Exception may arise when a db constrained has been violated
                    # (e.g. duplicate key)
                    safrs.log.warning(str(exc))
                    raise GenericError(str(exc))

            # object_id is the endpoint parameter, for example "UserId" for a User SAFRSObject
            obj_args = {instance.object_id: instance.jsonapi_id}
            # Retrieve the object json and return it to the client
            obj_data = self.get(**obj_args)
            response = make_response(obj_data, 201)
            # Set the Location header to the newly created object
            response.headers["Location"] = url_for(self.endpoint, **obj_args)

        return response
コード例 #5
0
ファイル: jsonapi.py プロジェクト: macleodbroad-wf/safrs
    def post(self, **kwargs):
        """
            responses :
                403:
                    description : This implementation does not accept client-generated IDs
                201:
                    description: Created
                202:
                    description : Accepted
                404:
                    description : Not Found
                409:
                    description : Conflict
            ---
            Add a child to a relationship
        """
        errors = []
        kwargs["require_child"] = True
        parent, relation = self.parse_args(**kwargs)

        json_response = request.get_jsonapi_payload()
        if not isinstance(json_response, dict):
            raise ValidationError("Invalid Object Type")
        data = json_response.get("data")

        if self.SAFRSObject.relationship.direction == MANYTOONE:
            # pylint: disable=len-as-condition
            if len(data) == 0:
                setattr(parent, self.SAFRSObject.relationship.key, None)
            if len(data) > 1:
                raise ValidationError(
                    "Too many items for a MANYTOONE relationship", 403)
            child_id = data[0].get("id")
            child_type = data[0].get("type")
            if not child_id or not child_type:
                raise ValidationError("Invalid data payload", 403)

            if child_type != self.child_class.__name__:
                raise ValidationError("Invalid type", 403)

            child = self.child_class.get_instance(child_id)
            setattr(parent, self.SAFRSObject.relationship.key, child)
            result = [child]

        else:  # direction is TOMANY => append the items to the relationship
            for item in data:
                if not isinstance(json_response, dict):
                    raise ValidationError("Invalid data type")
                child_id = item.get("id", None)
                if child_id is None:
                    errors.append("no child id {}".format(data))
                    safrs.log.error(errors)
                    continue
                child = self.child_class.get_instance(child_id)

                if not child:
                    errors.append("invalid child id {}".format(child_id))
                    safrs.log.error(errors)
                    continue
                if not child in relation:
                    relation.append(child)
            result = [item for item in relation]

        return jsonify({"data": result})
コード例 #6
0
ファイル: jsonapi.py プロジェクト: thomaxxl/safrs
    def delete(self, **kwargs):
        """
        summary : Delete {child_name} from {cls.relationship.key}
        description : Delete {child_name} items from the {parent_name} {cls.relationship.key} "{direction}" relationship
        responses:
            202 :
                description: Accepted
            204 :
                description: Request fulfilled, nothing follows
            200 :
                description: Success
            403 :
                description: Forbidden
            404 :
                description: Not Found
        ----
        Remove an item from a relationship
        """
        # pylint: disable=unused-variable
        # (parent is unused)
        parent, relation = self.parse_args(**kwargs)

        # No child id=> delete specified items from the relationship
        payload = request.get_jsonapi_payload()
        if not isinstance(payload, dict):
            raise ValidationError("Invalid Object Type")
        data = payload.get("data")

        if self.SAFRSObject.relationship.direction == MANYTOONE:
            # https://jsonapi.org/format/#crud-updating-to-one-relationships
            # We should only use patch to update
            # previous versions incorrectly implemented the jsonapi spec for updating manytoone relationships
            # keep things backwards compatible for now
            child = data
            if isinstance(data, list):
                if data and isinstance(data[0], dict):
                    # invalid, try to fix it by deleting the firs item from the list
                    safrs.log.warning(
                        "Invalid Payload to delete from MANYTOONE relationship"
                    )
                    data = data[0]
            if not isinstance(data, dict):
                raise ValidationError("Invalid data payload")
            child_id = data.get("id", None)
            child_type = data.get("type", None)

            if child_id is None or child_type is None:
                raise ValidationError("Invalid data payload",
                                      HTTPStatus.FORBIDDEN)

            if child_type != self.target._s_type:
                raise ValidationError("Invalid type", HTTPStatus.FORBIDDEN)

            child = self.target.get_instance(child_id)
            if child == relation and getattr(parent, self.rel_name,
                                             None) == child:
                # Delete the item from the many-to-one relationship
                delattr(parent, self.rel_name)
            else:
                safrs.log.warning("child not in relation")

        else:
            # https://jsonapi.org/format/#crud-updating-to-many-relationships
            children = data
            if not isinstance(data, list) or not children:
                raise ValidationError("Invalid data payload")
            for child in children:
                child_id = child.get("id", None)
                child_type = child.get("type", None)

                if not child_id or not child_type:
                    raise ValidationError("Invalid data payload",
                                          HTTPStatus.FORBIDDEN)

                if child_type != self.target._s_type:
                    raise ValidationError("Invalid type", HTTPStatus.FORBIDDEN)

                child = self.target.get_instance(child_id)
                if child in relation:
                    relation.remove(child)
                else:
                    safrs.log.warning(
                        f"Item with id {child_id} not in relation")

        return make_response(jsonify({}), HTTPStatus.NO_CONTENT)
コード例 #7
0
ファイル: jsonapi.py プロジェクト: thomaxxl/safrs
    def patch(self, **kwargs):
        """
        summary : Update {cls.relationship.key}
        description : Update the {parent_name} {cls.relationship.key} "{direction}" relationship
        responses:
            200 :
                description : Accepted
            204 :
                description : No Content
            403:
                description : Forbidden
            404 :
                description : Not Found
            409 :
                description : Conflict
        ----
        Update or create a relationship child item
        to be used to create or update one-to-many mappings
        but also works for many-to-many etc.

        # Updating To-One Relationships

        http://jsonapi.org/format/#crud-updating-to-one-relationships:
        A server MUST respond to PATCH requests to a URL
        from a to-one relationship link as described below

        The PATCH request MUST include a top-level member named data containing one of:
        a resource identifier object corresponding to the new related resource.
        null, to remove the relationship.
        """
        changed = False
        parent, relation = self.parse_args(**kwargs)
        payload = request.get_jsonapi_payload()
        data = payload.get("data")
        relation = getattr(parent, self.rel_name)
        obj_args = {self.parent_object_id: parent.jsonapi_id}

        if isinstance(data, dict):
            # https://jsonapi.org/format/#crud-updating-to-one-relationships
            # server MUST respond to PATCH requests to a URL from a to-one relationship link as described below.
            #   The PATCH request MUST include a top-level member named data containing one of:
            #   a resource identifier object corresponding to the new related resource.
            #   null, to remove the relationship.

            if self.SAFRSObject.relationship.direction != MANYTOONE:
                raise ValidationError(
                    "Provide a list to PATCH a TOMANY relationship")
            child = self._parse_target_data(data)
            if getattr(parent, self.rel_name) != child:
                # change the relationship, i.e. add the child
                setattr(parent, self.rel_name, child)
                obj_args[self.child_object_id] = child.jsonapi_id
                changed = True

        elif isinstance(
                data, list
        ) and not self.SAFRSObject.relationship.direction == MANYTOONE:
            """
            http://jsonapi.org/format/#crud-updating-to-many-relationships

            If a client makes a PATCH request to a URL from a to-many relationship link,
            the server MUST either completely replace every member of the relationship,
            return an appropriate error response if some resourcescan not be found
            or accessed, or return a 403 Forbidden response if complete
            replacement is not allowed by the server.
            """
            # first remove all items, then append the new items
            # if the relationship has been configured with lazy="dynamic"
            # then it is a subclass of AppenderBaseQuery and
            # we should empty the relationship by setting it to []
            # otherwise it is an instance of InstrumentedList and we have to empty it
            # (we could loop all items but this is slower for large collections)
            tmp_rel = []
            for child_data in data:
                child = self._parse_target_data(child_data)
                tmp_rel.append(child)

            if isinstance(relation,
                          sqlalchemy.orm.collections.InstrumentedList):
                relation[:] = tmp_rel
            else:
                setattr(parent, self.rel_name, tmp_rel)

        elif data is None and self.SAFRSObject.relationship.direction == MANYTOONE:
            # { data : null } //=> clear the relationship
            child = getattr(parent, self.SAFRSObject.relationship.key)
            if child:
                pass
            setattr(parent, self.rel_name, None)
        else:
            raise ValidationError(
                f'Invalid data object type "{type(data)}" for this "{self.SAFRSObject.relationship.direction}"" relationship'
            )

        # Create the patch response
        # https://jsonapi.org/format/#crud-updating-responses
        # 200 OK
        # If a server accepts an update but also changes the resource(s) in ways other than those specified by the request
        # (for example, updating the updated-at attribute or a computed sha), it MUST return a 200 OK response. The response
        # document MUST include a representation of the updated resource(s) as if a GET request was made to the request URL.
        # A server MUST return a 200 OK status code if an update is successful, the client’s current attributes remain up to date,
        # and the server responds only with top-level meta data. In this case the server MUST NOT include a representation of the updated resource(s).
        # 204 No Content
        # If an update is successful and the server doesn’t update any attributes besides those provided, the server MUST return
        # either a 200 OK status code and response document (as described above) or a 204 No Content status code with no response document.

        if data is None:
            # item removed from relationship => 202 accepted
            data, status_code = {}, HTTPStatus.NO_CONTENT
        elif changed:
            return self.get(**obj_args)
        else:
            # Nothing changed, reflect the data
            data, status_code = {"data": data}, HTTPStatus.OK

        return make_response(jsonify(data), status_code)
コード例 #8
0
ファイル: jsonapi.py プロジェクト: thomaxxl/safrs
    def post(self, **kwargs):
        """
        summary : Create {class_name}
        responses :
            403:
                description : Forbidden
            201:
                description: Created
            202:
                description : Accepted
            404:
                description : Not Found
            409:
                description : Conflict
        ---
        http://jsonapi.org/format/#crud-creating
        Creating Resources
        A resource can be created by sending a POST request to a URL
        that represents a collection of resources.
        The request MUST include a single resource object as primary data.
        The resource object MUST contain at least a type member.

        If a relationship is provided in the relationships member of the resource object,
        its value MUST be a relationship object with a data member.
        The value of this key represents the linkage the new resource is to have.

        Response:
        403: This implementation does not accept client-generated IDs
        201: Created
        202: Accepted
        404: Not Found
        409: Conflict

        Location Header identifying the location of the newly created resource
        Body : created object

        TODO:
        409 Conflict
          A server MUST return 409 Conflict when processing a POST request
          to create a resource with a client-generated ID that already exists.
          A server MUST return 409 Conflict when processing a POST request
          in which the resource object’s type is not among the type(s) that
          constitute the collection represented by the endpoint.
          A server SHOULD include error details and provide enough
          information to recognize the source of the conflict.
        """
        payload = request.get_jsonapi_payload()
        id = kwargs.get(self._s_object_id, None)
        if id is not None:
            # POSTing to an instance isn't jsonapi-compliant (https://jsonapi.org/format/#crud-creating-client-ids)
            # to do: modify Allow header
            raise ValidationError(f"POSTing to instance is not allowed {self}",
                                  status_code=HTTPStatus.METHOD_NOT_ALLOWED)

        # Create a new instance of the SAFRSObject
        data = payload.get("data")
        if data is None:
            raise ValidationError("Request contains no data")
        if isinstance(data, list):
            # http://springbot.github.io/json-api/extensions/bulk/
            # We should verify that the bulk extension is requested
            # Accept it by default now
            if not request.is_bulk:
                safrs.log.warning(
                    "Client sent a bulk POST but did not specify the bulk extension"
                )
            instances = []
            for item in data:
                instance = self._create_instance(item)
                instances.append(instance)
            resp_data = jsonify({"data": instances})
            location = None
        else:
            instance = self._create_instance(data)
            # object_id is the endpoint parameter, for example "UserId" for a User SAFRSObject
            obj_args = {instance._s_object_id: instance.jsonapi_id}
            # Retrieve the object json and return it to the client
            resp_data = self.get(**obj_args)
            location = f"{url_for(self.endpoint)}{instance.jsonapi_id}"

        response = make_response(resp_data, HTTPStatus.CREATED)
        # Set the Location header to the newly created object(s)
        if location:
            response.headers["Location"] = location

        return response