Source code for aiida_restapi.jsonapi.hooks

from __future__ import annotations

import typing as t

from aiida import orm
from aiida.common.exceptions import NotExistent

from .utils import IncludedItemParamsCache

IncludedItemParams = tuple[t.Union[str, int], str, dict[str, t.Any], dict[str, t.Any]]
# (id, type, attributes, foreign fields)


[docs] class BaseHook: """The base hook for JSON:API document customization.""" TYPE_MAP: dict[str, str] = {} FOREIGN_FIELDS: list[str] = [] INCLUDED_TYPE_MAP: dict[str, tuple[str, type[orm.Entity]]] = {}
[docs] @classmethod def split_resource( cls, result: dict[str, t.Any], resource_identity: str, resource_type: str, ) -> tuple[str, dict[str, t.Any], dict[str, t.Any]]: """Split the result into identifier, attributes, and foreign fields. :param result: The raw result of the resource. :type result: dict[str, t.Any] :param resource_identity: The identity field of the resource. :type resource_identity: str :param resource_type: The type of the resource. :type resource_type: str :return: A tuple containing the identifier, attributes, and foreign fields. :rtype: tuple[str, dict[str, t.Any], dict[str, t.Any]] """ if not (identifier := str(result.get(resource_identity, ''))): raise ValueError(f"Missing '{resource_identity}' in model dump for resource_type={resource_type!r}") attributes: dict[str, t.Any] = {} foreign_fields: dict[str, t.Any] = {} for key, value in result.items(): if key in cls.FOREIGN_FIELDS: foreign_fields[key] = value else: attributes[key] = value return identifier, attributes, foreign_fields
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: """Return relationships dictionary for the resource. :param foreign_fields: The foreign fields of the resource. :type foreign_fields: dict[str, t.Any] :param resource_type: The type of the resource. :type resource_type: str :param base_api_url: The base URL of the API. :type base_api_url: str :param url_id: The URL identifier of the resource. :type url_id: str :return: A dictionary of relationships. :rtype: dict[str, dict[str, t.Any]] """ return {}
[docs] @classmethod def include( cls, *, foreign_fields: dict[str, t.Any], include: list[str], cache: IncludedItemParamsCache | None = None, ) -> list[IncludedItemParams]: included: list[IncludedItemParams] = [] for item in include: if item not in cls.FOREIGN_FIELDS: raise NotExistent(f"Include field '{item}' not recognized; valid fields: {cls.FOREIGN_FIELDS}") if not (identifier := foreign_fields.get(item, None)): continue if included_item := cls._build_included_item( identifier, *cls.INCLUDED_TYPE_MAP[item], cache=cache, ): included.append(included_item) return included
[docs] @classmethod def _build_included_item( cls, resource_id: str | int, resource_type: str, orm_class: type[orm.Entity], cache: IncludedItemParamsCache | None = None, ) -> IncludedItemParams | None: """Build an included item parameters tuple. :param resource_id: The identifier of the resource. :type resource_id: str | int :param resource_type: The type of the resource. :type resource_type: str :param orm_class: The ORM class of the resource. :type orm_class: type[orm.Entity] :param cache: An optional cache for included resources. :type cache: IncludedItemParamsCache | None :return: A tuple containing identifier, type, attributes, and foreign fields of the included resource, or None if resource_id is None. :rtype: IncludedItemParams | None """ if resource_id is None: return None bucket = cache.bucket(resource_type) if cache is not None else None cache_key = str(resource_id) if bucket is not None and cache_key in bucket: return None node: orm.Entity = orm_class.collection.get(**{orm_class.identity_field: resource_id}) node_dict = node.serialize(minimal=True) identifier, attributes, foreign_fields = cls.split_resource( node_dict, node.identity_field, resource_type, ) included_item: IncludedItemParams = (identifier, resource_type, attributes, foreign_fields) if bucket is not None: bucket[cache_key] = included_item return included_item
[docs] class ResourceHook(BaseHook): """A hook for JSON:API resource customization."""
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: return { 'collection': { 'links': { 'related': f'{base_api_url}/{resource_type}', } } }
[docs] class EntityHook(ResourceHook): """A hook for entity-specific JSON:API customization.""" TYPE_MAP: dict[str, str] = { 'users': 'user', 'computers': 'computer', 'nodes': 'node', 'groups': 'group', } INCLUDED_TYPE_MAP: dict[str, tuple[str, type[orm.Entity]]] = { 'user': ('users', orm.User), 'computer': ('computers', orm.Computer), 'node': ('nodes', orm.Node), 'group': ('groups', orm.Group), }
[docs] class ComputerHook(EntityHook): """A hook for computer-specific JSON:API customization."""
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: extra = { 'metadata': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/metadata', } }, } return ( super().relationships( foreign_fields=foreign_fields, resource_type=resource_type, base_api_url=base_api_url, url_id=url_id, ) | extra )
[docs] class GroupHook(EntityHook): """A hook for group-specific JSON:API customization.""" FOREIGN_FIELDS: list[str] = ['user']
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: extra = { 'user': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/user', }, 'data': { 'id': str(foreign_fields.get('user')), 'type': 'users', }, }, 'nodes': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/nodes', } }, 'extras': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/extras', } }, } return ( super().relationships( foreign_fields=foreign_fields, resource_type=resource_type, base_api_url=base_api_url, url_id=url_id, ) | extra )
[docs] class NodeHook(EntityHook): """A hook for node-specific JSON:API customization.""" FOREIGN_FIELDS: list[str] = ['user', 'computer']
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: extra = { 'user': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/user', }, 'data': { 'id': str(foreign_fields.get('user')), 'type': 'users', }, }, } # Not all nodes have an associated computer computer_id = foreign_fields.get('computer', None) if computer_id is not None: extra['computer'] = { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/computer', }, 'data': { 'id': str(computer_id), 'type': 'computers', }, } extra |= { 'groups': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/groups', } }, 'attributes': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/attributes', } }, 'extras': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/extras', } }, 'repository_metadata': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/repo/metadata', } }, 'incoming': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/links?direction=incoming', } }, 'outgoing': { 'links': { 'related': f'{base_api_url}/{resource_type}/{url_id}/links?direction=outgoing', } }, } return ( super().relationships( foreign_fields=foreign_fields, resource_type=resource_type, base_api_url=base_api_url, url_id=url_id, ) | extra )
[docs] class LinkHook(BaseHook): """A hook for link-specific JSON:API customization.""" FOREIGN_FIELDS: list[str] = ['source', 'target'] INCLUDED_TYPE_MAP: dict[str, tuple[str, type[orm.Entity]]] = { 'source': ('nodes', orm.Node), 'target': ('nodes', orm.Node), }
[docs] @classmethod def relationships( cls, *, foreign_fields: dict[str, t.Any], resource_type: str, base_api_url: str, url_id: str, ) -> dict[str, dict[str, t.Any]]: extra = {} if source := foreign_fields.get('source', None): extra['source'] = { 'links': { 'related': f'{base_api_url}/nodes/{source}', }, 'data': { 'id': str(source), 'type': 'nodes', }, } if target := foreign_fields.get('target', None): extra['target'] = { 'links': { 'related': f'{base_api_url}/nodes/{target}', }, 'data': { 'id': str(target), 'type': 'nodes', }, } return extra