Source code for aiohttp_json_api.utils

"""Utilities related to JSON API."""

import asyncio
import typing
from collections import defaultdict, OrderedDict

from aiohttp import web
from aiohttp.web_response import Response
import trafaret as t

from .common import JSONAPI, JSONAPI_CONTENT_TYPE
from .encoder import json_dumps
from .errors import Error, ErrorList, ValidationError
from .helpers import first, is_collection


[docs]def jsonapi_response(data, *, status=web.HTTPOk.status_code, reason=None, headers=None, dumps=None): """ Return JSON API response. :param data: Rendered JSON API document :param status: HTTP status of JSON API response :param reason: Readable reason of error response :param headers: Headers :param dumps: Custom JSON dumps callable :return: Response instance """ if not callable(dumps): dumps = json_dumps body = dumps(data).encode('utf-8') return Response(body=body, status=status, reason=reason,
headers=headers, content_type=JSONAPI_CONTENT_TYPE)
[docs]async def get_compound_documents(resources, ctx): """ Get compound documents of resources. .. seealso:: http://jsonapi.org/format/#fetching-includes Fetches the relationship paths *paths*. :param resources: A list with the primary data (resources) of the compound response document. :param ctx: A web Request context :returns: A two tuple with a list of the included resources and a dictionary, which maps each resource (primary and included) to a set with the names of the included relationships. """ relationships = defaultdict(set) compound_documents = OrderedDict() collection = (resources,) if type(resources) in ctx.registry else resources for path in ctx.include: if path and collection: rest_path = path nested_collection = collection while rest_path and nested_collection: schema_cls, controller_cls = \ ctx.registry[first(nested_collection)] resource_type = schema_cls.opts.resource_type if rest_path in relationships[resource_type]: break field = schema_cls.get_relationship_field( rest_path[0], source_parameter='include' ) controller = controller_cls(ctx) nested_collection = await controller.fetch_compound_documents( field, nested_collection, rest_path=rest_path[1:] ) for relative in nested_collection: compound_documents.setdefault( ctx.registry.ensure_identifier(relative), relative ) relationships[resource_type].add(rest_path) rest_path = rest_path[1:]
return compound_documents, relationships
[docs]def serialize_resource(resource, ctx): """ Serialize resource by schema. :param resource: Resource instance :param ctx: Request context :return: Serialized resource """ schema_cls, _ = ctx.registry[resource]
return schema_cls(ctx).serialize_resource(resource)
[docs]async def render_document(data, included, ctx, *, pagination=None, links=None) -> typing.MutableMapping: """ Render JSON API document. :param data: One or many resources :param included: Compound documents :param ctx: Request context :param pagination: Pagination instance :param links: Additional links :return: Rendered JSON API document """ document = OrderedDict() if is_collection(data, exclude=(ctx.schema.opts.resource_cls,)): document['data'] = [serialize_resource(r, ctx) for r in data] else: document['data'] = serialize_resource(data, ctx) if data else None if ctx.include and included: document['included'] = \ [serialize_resource(r, ctx) for r in included.values()] document.setdefault('links', OrderedDict()) document['links']['self'] = str(ctx.request.url) if links is not None: document['links'].update(links) meta_object = ctx.request.app[JSONAPI]['meta'] pagination = pagination or ctx.pagination if pagination or meta_object: document.setdefault('meta', OrderedDict()) if pagination is not None: document['links'].update(pagination.links()) document['meta'].update(pagination.meta()) if meta_object: document['meta'].update(meta_object) jsonapi_info = ctx.request.app[JSONAPI]['jsonapi'] if jsonapi_info: document['jsonapi'] = jsonapi_info
return document
[docs]def error_to_response(request: web.Request, error: typing.Union[Error, ErrorList]): """ Convert an :class:`Error` or :class:`ErrorList` to JSON API response. :arg ~aiohttp.web.Request request: The web request instance. :arg typing.Union[Error, ErrorList] error: The error, which is converted into a response. :rtype: ~aiohttp.web.Response """ if not isinstance(error, (Error, ErrorList)): raise TypeError('Error or ErrorList instance is required.') return jsonapi_response( { 'errors': [error.as_dict] if isinstance(error, Error) else error.as_dict, 'jsonapi': request.app[JSONAPI]['jsonapi'] }, status=error.status
)
[docs]def validate_uri_resource_id(schema, resource_id): """ Validate resource ID from URI. :param schema: Resource schema :param resource_id: Resource ID """ field = getattr(schema, '_id', None) if field is None: try: t.Int().check(resource_id) except t.DataError as exc: raise ValidationError(detail=str(exc).capitalize(), source_parameter='id') else: try: field.pre_validate(schema, resource_id, sp=None) except ValidationError as exc: exc.source_parameter = 'id'
raise exc