예제 #1
0
 def _s_object_id(cls):
     """
     :return: the Flask url parameter name of the object, e.g. UserId
     :rtype: string
     """
     # pylint: disable=no-member
     return cls.__name__ + get_config("OBJECT_ID_SUFFIX")
예제 #2
0
def documented_api_method(method):
    '''
        Decorator to expose functions in the REST API:
        When a method is decorated with documented_api_method, this means
        it becomes available for use through HTTP POST (i.e. public)
    '''
    USE_API_METHODS = get_config('USE_API_METHODS')
    if USE_API_METHODS:
        try:
            api_doc = parse_object_doc(method)
        except yaml.scanner.ScannerError:
            safrs.LOGGER.error('Failed to parse documentation for %s', method)
        setattr(method, REST_DOC, api_doc)
    return method
예제 #3
0
 def get_endpoint(cls, url_prefix=None, type=None):
     """
     :param url_prefix: URL prefix used by the app
     :param type: endpoint type, e.g. "instance"
     :return: the API endpoint
     :rtype: str
     """
     if url_prefix is None:
         url_prefix = cls.url_prefix
     if type == "instance":
         INSTANCE_ENDPOINT_FMT = get_config("INSTANCE_ENDPOINT_FMT")
         endpoint = INSTANCE_ENDPOINT_FMT.format(url_prefix, cls._s_type)
     else:  # type = 'collection'
         endpoint = "{}api.{}".format(url_prefix, cls._s_type)
     return endpoint
예제 #4
0
 def _documented_api_method(method):
     """
         :param method:
         add metadata to the method:
             REST_DOC: swagger documentation
             HTTP_METHODS: the http methods (GET/POST/..) used to call this method
     """
     USE_API_METHODS = get_config("USE_API_METHODS")
     if USE_API_METHODS:
         try:
             api_doc = parse_object_doc(method)
         except yaml.scanner.ScannerError:
             safrs.log.error("Failed to parse documentation for %s", method)
         setattr(method, REST_DOC, api_doc)
         setattr(method, HTTP_METHODS, http_methods)
     return method
예제 #5
0
    def _s_get_related(self):
        """
        :return: dict of relationship names -> [related instances]

        http://jsonapi.org/format/#fetching-includes

        Inclusion of Related Resources
        Multiple related resources can be requested in a comma-separated list:
        An endpoint MAY return resources related to the primary data by default.
        An endpoint MAY also support an include request parameter to allow
        the client to customize which related resources should be returned.
        In order to request resources related to other resources,
        a dot-separated path for each relationship name can be specified

        All related instances are stored in the `Included` class so we don't have to walk
        the relationships twice

        Request parameter example:
            include=friends.books_read,friends.books_written
        """
        # included_list contains a list of relationships to include
        # it may have been set previously by Included() when called recursively
        # if it's not set, parse the include= request param here
        # included_list example: ['friends.books_read', 'friends.books_written']
        included_list = getattr(self, "included_list", None)
        if included_list is None:
            # Multiple related resources can be requested in a comma-separated list
            included_csv = request.args.get("include",
                                            safrs.SAFRS.DEFAULT_INCLUDED)
            included_list = [inc for inc in included_csv.split(",") if inc]

        excluded_csv = request.args.get("exclude", "")
        excluded_list = excluded_csv.split(",")
        # In order to recursively request related resources
        # a dot-separated path for each relationship name can be specified
        included_rels = {i.split(".")[0] for i in included_list}
        relationships = dict()

        for rel_name in included_rels:
            """
            If a server is unable to identify a relationship path or does not support inclusion of resources from a path,
            it MUST respond with 400 Bad Request.
            """
            if rel_name != safrs.SAFRS.INCLUDE_ALL and rel_name not in self._s_relationships:
                raise GenericError(
                    "Invalid Relationship '{}'".format(rel_name),
                    status_code=400)

        for rel_name, relationship in self._s_relationships.items():
            """
            http://jsonapi.org/format/#document-resource-object-relationships:

            The value of the relationships key MUST be an object (a “relationships object”).
            Members of the relationships object (“relationships”) represent
            references from the resource object in which it’s defined to other resource objects.

            Relationships may be to-one or to-many.

            A “relationship object” MUST contain at least one of the following:

            - links: a links object containing at least one of the following:
                - self: a link for the relationship itself (a “relationship link”).
                This link allows the client to directly manipulate the relationship.
                - related: a related resource link
            - data: resource linkage
            - meta: a meta object that contains non-standard meta-information
                    about the relationship.
            A relationship object that represents a to-many relationship
            MAY also contain pagination links under the links member, as described below.
            SAFRS currently implements links with self
            """
            meta = {}
            rel_name = relationship.key
            data = [] if relationship.direction in (ONETOMANY,
                                                    MANYTOMANY) else None
            if rel_name in excluded_list:
                # TODO: document this
                # continue
                pass
            if rel_name in included_rels or safrs.SAFRS.INCLUDE_ALL in included_list:
                # next_included_list contains the recursive relationship names
                next_included_list = [
                    inc_item.split(".")[1:] for inc_item in included_list
                    if inc_item.startswith(rel_name + ".")
                ]
                if relationship.direction == MANYTOONE:
                    # manytoone relationship contains a single instance
                    rel_item = getattr(self, rel_name)
                    if rel_item:
                        # create an Included instance that will be used for serialization eventually
                        data = Included(rel_item, next_included_list)
                elif relationship.direction in (ONETOMANY, MANYTOMANY):
                    # manytoone relationship contains a list of instances
                    # Data is optional, it's also really slow for large sets!
                    data = []
                    rel_query = getattr(self, rel_name)
                    limit = request.page_limit
                    if not get_config("ENABLE_RELATIONSHIPS"):
                        meta[
                            "warning"] = "ENABLE_RELATIONSHIPS set to false in config.py"
                    elif rel_query:
                        # todo: chekc if lazy=dynamic
                        # In order to work with the relationship as with Query,
                        # you need to configure it with lazy='dynamic'
                        # "limit" may not be possible !
                        if getattr(rel_query, "limit", False):
                            count = rel_query.count()
                            rel_query = rel_query.limit(limit)
                            if rel_query.count() >= get_config(
                                    "BIG_QUERY_THRESHOLD"):
                                warning = 'Truncated result for relationship "{}",consider paginating this request'.format(
                                    rel_name)
                                safrs.log.warning(warning)
                                meta["warning"] = warning
                            items = rel_query.all()
                        else:
                            items = list(rel_query)
                            count = len(items)
                        meta["count"] = count
                        meta["limit"] = limit
                        for rel_item in items:
                            data.append(Included(rel_item, next_included_list))
                else:  # pragma: no cover
                    # shouldn't happen!!
                    safrs.log.error(
                        "Unknown relationship direction for relationship {}: {}"
                        .format(rel_name, relationship.direction))

            rel_link = urljoin(self._s_url, rel_name)
            links = dict(self=rel_link)
            rel_data = dict(links=links)

            rel_data["data"] = data
            if meta:
                rel_data["meta"] = meta
            relationships[rel_name] = rel_data

        return relationships