jsonapy.base
Complete reference of resource definition
This section explains how to define a resource, its fields and its links. See
BaseResource.jsonapi_dict()
for a documentation on exporting objects into
JSON:API format.
Fields
Attributes
You can define the fields to export with annotations:
from jsonapy import BaseResource
class MyResource(BaseResource):
id: int
attr1: str
attr2: bool
You must annotate the id
member, even if it is only used for identification
and not considered as an attribute.
A field cannot be named after reserved names which are:
- the already-defined members of the
BaseResource
class; "type"
, a reserved identifier object member. By default, the type name of the resource is the name of the class. It can be can overwritten by specifying theresource_name
attribute of theMeta
inner class:
class NamedResource(BaseResource):
id: int
class Meta:
resource_name = "myResourceName"
"links"
: a member name used by the JSON:API specification."relationships"
: a member name used by the JSON:API specification.
Relationships
When a field has a resource class type hint, it is considered as a relationship.
class MyOtherResource(BaseResource):
id: int
related_main_resource: MyResource
The related_main_resource
is a relationship and will be correctly handled
when dumping the instances. See BaseResource.jsonapi_dict()
for more the
documentation on exporting the objects into JSON:API format.
Meta fields
The Meta
inner class lets you define non-standard attributes:
meta_attributes
: A set of meta attributes names. These attributes will be exported in themeta
object (alongside"id"
,"type"
,"attributes"
,"relationships"
and"links"
).identifier_meta_attributes
: A set of identifier meta fields names, these will be in themeta
object when exporting resource identifier objects (alongside"type"
and"id"
).
Link registering
To integrate the endpoints of an API in the resources, you can register factories functions that will be used to produce resource's links. It can be done in two ways:
- If the factory function does not use any dependency, it can be defined
directly in the
Meta
inner class:
class LinkedResource(BaseResource):
id: int
class Meta:
links_factories = {
"self": lambda x: f"/linked/{x}"
}
- If the model is imported in the module where routing functions are defined, factories functions can be registered with a class method. For example, if you are using Starlette, it could be:
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.routes import Route
from functools import partial
def linke_resource(request):
...
app = Starlette(debug=True, routes=[
Route('/linked/{id}', linked_resources),
])
def make_link(route, request **path_params):
return Request.url_for(request, route, **path_params)
LinkedResource.register_link_factory("self", partial(make_link, "linked_resources"))
For the relationships links, prefix the links names with the relationship name:
BResource.register_link_factory(
"related__self",
lambda x: f"/bresource/{x}/relationships/related"
)
See BaseResource.register_link_factory()
for more information
Abstract resource and inheritance
A resource must export an "id"
member, unless it is an abstract resource.
An abstract resource can be inherited to create concrete resources, we indicate
that a resource is abstract with the Meta
innerclass:
class AbstractResource(BaseResource):
attr: str
class Meta:
is_abstract = True
A concrete resource definition without "id"
member will raise an AttributeError
.
When a resource subclasses another resource, all fields are copied to the sub-resource,
but the Meta
inner class is not inherited, so a resource is concrete by default.
Accessing configuration and meta information about resources
These resource classes are used in the following examples.
class AResource(BaseResource):
id: int
class BResource(BaseResource):
id: int
name: str
related: AResource
class Meta:
links_factories = {
"self": lambda x: f"/b/{x}",
"related__self": lambda x: f"/b/{x}/relationships/related"}
Basic information about fields
Some metadata about a resource can be accessed through top level functions applied on a resource class:
from jsonapy.functions import fields_types, relationships_names, attributes_names
fields_types(BResource)
# {'id': int, 'name': str, 'related': __main__.AResource}
relationships_names(BResource)
# {'related'}
attributes_names(BResource)
# {'name'}
See the jsonapy.functions
module for more information about these functions
and refer to the following sections to know more about special metadata class
attributes.
Atomic and relationships fields
When a field is a instance of BaseResource
, it is considered as a
relationship field. The other fields are considered as "atomic": the id
used
for identification, and the attributes that are exported in the "attributes"
object.
Some special attributes provide the sets of atomic and relationships fields names.
BResource.__fields_types__
# {'id': int, 'name': str, 'related': __main__.AResource}
BResource.__atomic_fields_set__
# {'id', 'name'}
BResource.__relationships_fields_set__
# {'related'}
Configuration special attributes
Summary of attributes which can be defined in the Meta
inner class:
is_abstract
: A boolean indicating the model is aimed to be instantiated.resource_name
: The resource type name.meta_attributes
: A set of non-standard attributes names which will be exported in themeta
object.identifier_meta_attributes
: A set containing extra non standard fields names that contain extra meta-information for identification.links_factories
: A dictonary containing factories functions used to generate resources links when they are exported. The keys are the names of the links and the values their factories functions.
During runtime, these metadata can be accessed with special attributes directly
on the resource classes. For example, the value of is_abstract
is available
on the __is_abstract__
attribute. The Meta
inner class is not accessible
during runtime. See the BaseResourceMeta
metaclass for more information about
these attributes initialization.
Creating a resource class dynamically
This module provides a create_resource()
functions for creating resources
classes at runtime. We can recreate BResource
:
BResource = create_resource(
"BResource",
{"links_factories":
{"self": lambda x: f"/b/{x}",
"related__self": lambda x: f"/b/{x}/relationships/related"}},
id=int,
name=str,
related=AResource)
attributes_names(BResource)
# {'name'}
relationships_names(BResource)
# {'related'}
fields_types(BResource)
# {'id': int, 'name': str, 'related': __main__.AResource}
BResource.__links_factories__
# {'self': <function __main__.<lambda>(x)>,
# 'related__self': <function __main__.<lambda>(x)>}
View Source
# Copyright (c) 2021 Guillaume Fayard # This library is licensed under the MIT license # For a complete copy of the license, see the LICENSE file. """ # Complete reference of resource definition This section explains how to define a resource, its fields and its links. See `BaseResource.jsonapi_dict()` for a documentation on exporting objects into JSON:API format. ## Fields ### Attributes You can define the [fields](https://jsonapi.org/format/#document-resource-object-fields) to export with annotations: ```python from jsonapy import BaseResource class MyResource(BaseResource): id: int attr1: str attr2: bool ``` You must annotate the `id` member, even if it is only used for identification and not considered as an attribute. A field cannot be named after reserved names which are: - the already-defined members of the `BaseResource` class; - `"type"`, a reserved identifier object member. By default, the type name of the resource is the name of the class. It can be can overwritten by specifying the `resource_name` attribute of the `Meta` inner class: ```python class NamedResource(BaseResource): id: int class Meta: resource_name = "myResourceName" ``` - `"links"`: a member name used by the JSON:API specification. - `"relationships"`: a member name used by the JSON:API specification. ### Relationships When a field has a resource class type hint, it is considered as a relationship. ```python class MyOtherResource(BaseResource): id: int related_main_resource: MyResource ``` The `related_main_resource` is a relationship and will be correctly handled when dumping the instances. See `BaseResource.jsonapi_dict()` for more the documentation on exporting the objects into JSON:API format. ### Meta fields The `Meta` inner class lets you define non-standard attributes: * `meta_attributes`: A set of meta attributes names. These attributes will be exported in the [`meta`](https://jsonapi.org/format/#document-meta) object (alongside `"id"`, `"type"`, `"attributes"`, `"relationships"` and `"links"`). * `identifier_meta_attributes`: A set of identifier meta fields names, these will be in the `meta` object when exporting [resource identifier objects](https://jsonapi.org/format/#document-resource-identifier-objects) (alongside `"type"` and `"id"`). ## Link registering To integrate the endpoints of an API in the resources, you can register factories functions that will be used to produce resource's links. It can be done in two ways: - If the factory function does not use any dependency, it can be defined directly in the `Meta` inner class: ```python class LinkedResource(BaseResource): id: int class Meta: links_factories = { "self": lambda x: f"/linked/{x}" } ``` - If the model is imported in the module where routing functions are defined, factories functions can be registered with a class method. For example, if you are using [Starlette](https://www.starlette.io/), it could be: ```python from starlette.applications import Starlette from starlette.requests import Request from starlette.routes import Route from functools import partial def linke_resource(request): ... app = Starlette(debug=True, routes=[ Route('/linked/{id}', linked_resources), ]) def make_link(route, request **path_params): return Request.url_for(request, route, **path_params) LinkedResource.register_link_factory("self", partial(make_link, "linked_resources")) ``` For the relationships links, prefix the links names with the relationship name: ```python BResource.register_link_factory( "related__self", lambda x: f"/bresource/{x}/relationships/related" ) ``` See `BaseResource.register_link_factory()` for more information ## Abstract resource and inheritance A resource must export an `"id"` member, unless it is an abstract resource. An abstract resource can be inherited to create concrete resources, we indicate that a resource is abstract with the `Meta` innerclass: ```python class AbstractResource(BaseResource): attr: str class Meta: is_abstract = True ``` A concrete resource definition without `"id"` member will raise an `AttributeError`. When a resource subclasses another resource, all fields are copied to the sub-resource, but the `Meta` inner class is not inherited, so a resource is concrete by default. ## Accessing configuration and meta information about resources These resource classes are used in the following examples. ```python class AResource(BaseResource): id: int class BResource(BaseResource): id: int name: str related: AResource class Meta: links_factories = { "self": lambda x: f"/b/{x}", "related__self": lambda x: f"/b/{x}/relationships/related"} ``` ### Basic information about fields Some metadata about a resource can be accessed through top level functions applied on a resource class: ```python from jsonapy.functions import fields_types, relationships_names, attributes_names fields_types(BResource) # {'id': int, 'name': str, 'related': __main__.AResource} relationships_names(BResource) # {'related'} attributes_names(BResource) # {'name'} ``` See the `jsonapy.functions` module for more information about these functions and refer to the following sections to know more about special metadata class attributes. ### Atomic and relationships fields When a field is a instance of `BaseResource`, it is considered as a relationship field. The other fields are considered as "atomic": the `id` used for identification, and the attributes that are exported in the `"attributes"` object. Some special attributes provide the sets of atomic and relationships fields names. ```python BResource.__fields_types__ # {'id': int, 'name': str, 'related': __main__.AResource} BResource.__atomic_fields_set__ # {'id', 'name'} BResource.__relationships_fields_set__ # {'related'} ``` ### Configuration special attributes Summary of attributes which can be defined in the `Meta` inner class: - `is_abstract`: A boolean indicating the model is aimed to be instantiated. - `resource_name`: The resource type name. - `meta_attributes`: A set of non-standard attributes names which will be exported in the [`meta`](https://jsonapi.org/format/#document-meta) object. - `identifier_meta_attributes`: A set containing extra non standard fields names that contain extra meta-information for identification. - `links_factories`: A dictonary containing factories functions used to generate resources links when they are exported. The keys are the names of the links and the values their factories functions. During runtime, these metadata can be accessed with special attributes directly on the resource classes. For example, the value of `is_abstract` is available on the `__is_abstract__` attribute. The `Meta` inner class is not accessible during runtime. See the `BaseResourceMeta` metaclass for more information about these attributes initialization. ## Creating a resource class dynamically This module provides a `create_resource()` functions for creating resources classes at runtime. We can recreate `BResource`: ```python BResource = create_resource( "BResource", {"links_factories": {"self": lambda x: f"/b/{x}", "related__self": lambda x: f"/b/{x}/relationships/related"}}, id=int, name=str, related=AResource) attributes_names(BResource) # {'name'} relationships_names(BResource) # {'related'} fields_types(BResource) # {'id': int, 'name': str, 'related': __main__.AResource} BResource.__links_factories__ # {'self': <function __main__.<lambda>(x)>, # 'related__self': <function __main__.<lambda>(x)>} ``` """ import collections.abc import json from typing import Any from typing import Callable from typing import Dict from typing import Iterable from typing import Literal from typing import Mapping from typing import Optional from typing import Set from typing import TYPE_CHECKING from typing import Tuple from typing import Type from typing import TypeVar from typing import Union from jsonapy import utils __all__ = ("BaseResource", "create_resource", "BaseResourceMeta") def _validate_link_name(klass, name): """Check if the link name is consistent with the resource class. If the link name is a relationship-qualified name, check if the relationship exists. Else raise a `ValueError`. """ split_name = name.split("__") if len(split_name) > 1: relationship_name = split_name[0] if relationship_name not in klass.__relationships_fields_set__: raise ValueError(f"'{relationship_name}' is not a valid relationship for {klass.__name__}.") class BaseResourceMeta(type): """Metaclass of resource classes. Instantiate a resource class and perform some initialization and checks. ###### Initialization ###### The following class attributes are initialized here: - `__fields_types__`: an alias for `__annotations__` - `__atomic_fields_set__`: a set containing the names of fields whose type is not a instance of `BaseResourceMeta` (the names of the non resource fields). - `__relationships_fields_set__`: a set containing the names of the fields referring another resource. ###### Extraction of `Meta` attributes ###### This metaclass converts the `Meta` inner class of the defined models into special attributes: - `__is_abstract__`: the `Meta.is_abstract` attribute if defined, else `False`. - `__resource_name__`: the `Meta.resource_name` attribute if defined, else the name of the resource class. - `__identifier_meta_attributes__`: the `Meta.identifier_meta_attributes` attribute if defined, else an empty set. - `___meta_attributes__`: the `Meta.meta_attributes` attribute if defined, else an empty set. - `__links_factories__`: the `Meta.links_factories` attribute if defined, else an empty dictionary. If a link name is not valid, a `ValueError` is raised. """ def __new__(mcs, name, bases, namespace): try: meta = namespace.pop("Meta").__dict__ except KeyError: meta = {} cls = super().__new__(mcs, name, bases, namespace) # forbidden fields, see https://jsonapi.org/format/#document-resource-object-fields forbidden_fields = {"type", "links", "relationships"} # identifier fields, see https://jsonapi.org/format/#document-resource-identifier-objects identifier_fields = {"type", "id"} cls.__annotations__ = { name: type_ for klass in cls.mro() if isinstance(klass, BaseResourceMeta) for (name, type_) in klass.__annotations__.items() } # members special attributes annotations_items = cls.__annotations__.items() cls.__fields_types__ = cls.__annotations__ cls.__atomic_fields_set__ = { name for name, type_ in annotations_items if not utils.is_type_hint_instance_of(type_, mcs) } - forbidden_fields cls.__relationships_fields_set__ = { name for name, type_ in annotations_items if utils.is_type_hint_instance_of(type_, mcs) } links_factories = {} for name, factory in meta.get("links_factories", {}).items(): _validate_link_name(cls, name) links_factories[name] = factory # meta special attributes cls.__links_factories__ = links_factories cls.__is_abstract__ = meta.get("is_abstract", False) cls.__resource_name__ = meta.get("resource_name", cls.__name__) cls.__identifier_meta_attributes__ = set(meta.get("identifier_meta_attributes", set())) cls.__meta_attributes__ = set(meta.get("meta_attributes", set())) if not cls.__is_abstract__ and "id" not in cls.__annotations__: raise AttributeError("A Resource must have an 'id' attribute.") # utilitarian private attributes cls._forbidden_fields = forbidden_fields cls._identifier_fields = identifier_fields return cls class BaseResource(metaclass=BaseResourceMeta): """Base class for defining resources. See the top of the `jsonapy.base` module for a doumentation on resource definition. This class is instantiated by the `BaseResourceMeta` metaclass. """ if TYPE_CHECKING: # for IDE, provided by metaclass __fields_types__: Dict[str, type] __atomic_fields_set__: Set[str] __relationships_fields_set__: Set[str] __resource_name__: str __is_abstract__: bool __identifier_meta_attributes__: Set[str] __links_factories__: Dict[str, Callable[..., str]] _identifier_fields: Set[str] _forbidden_fields: Set[str] # must be provided by subclasses id: Any class Meta: is_abstract: bool = True resource_name: str identifier_meta_attributes: Set[str] links_factories: Dict[str, Callable[..., str]] def __init__(self, **kwargs): """Automatically set all passed arguments. Take keyword arguments only and raise a `ValueError` if a parameter tries to reassign an already defined member (like the `jsonapi_dict()`. """ errors = [] for name in kwargs: if (hasattr(self, name) and name not in self.__fields_types__ or name in self._forbidden_fields): errors.append(f" This attribute name is reserved: '{name}'.") if errors: raise ValueError("\n" + "\n".join(errors)) for k, v in kwargs.items(): setattr(self, k, v) ########################################################################### # P U B L I C A P I # ########################################################################### def jsonapi_dict( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Mapping[str, Union[str, Mapping[str, Any]]]] = None, relationships: Optional[Dict] = None, ) -> Dict: """Export the object as a dictionary in compliance with JSON:API specification. ###### Parameters ###### - `required_attributes`: an iterable containing the fields names to include in dumped data. If all fields are required, provide the `"__all__"` literal instead of an iterable. - `links`: a dictionary containing links payload to include in data. The keys are the links names to dump, and the values are dictionaries containing keyword arguments to pass to the factory function. Raw strings can also be provided for unregistered links names. - `relationships`: a dictionary specifying the relationships to include, if the identifier object must be exported, and which links must be dumped. The keys must be the relationships names and the value of each key is another dict containing two keys: + `"data"`: a boolean indicating if an identifier object must be included. If there is identifier meta attributes, they are also exported. + `"links"`: a dictionary in the same shape as the `links` argument. ###### Returned value ###### A dictionary representing the object in compliance with the JSON:API specification. ###### Errors raised ###### A `ValueError` is raised if: - An attribute name in `required_attributes` argument is invalid. - A link name in `links` argument is not registered. - A key of the `relationship` argument does not refer to a valid related resource. - A link name of a specified relationship is not registered. ###### Examples ###### Given ```python aresource = AResource(id=1) bresource = BResource(id=1, name="foo", related=aresource) ``` A classic dump of `bresource` would be: ```python bresource.jsonapi_dict( required_attributes="__all__", links={"self": {"x": bresource.id}}, relationships={ "related":{ "data": True, "links": {"self": {"x": bresource.id}}}}) # {'type': 'BResource', # 'id': 1, # 'attributes': {'name': 'foo'}, # 'relationships': {'related': # {'links': {'self': '/b/1/relationships/related'}, # 'data': {'type': 'AResource', 'id': 1}}}, # 'links': {'self': '/b/1'}} ``` An export example with a raw string link: ```python aresource.jsonapi_dict( "__all__", {"self": f"/a/{aresource.id}"}) # {'type': 'AResource', 'id': 1, 'links': {'self': '/a/1'}} ``` """ if not hasattr(self, "id"): raise AttributeError(f"This '{self.__class__.__name__}' object has no id.") data = { "type": self.__resource_name__, "id": self.id, } filtered_attributes, meta_attributes = self._filtered_attributes(required_attributes) if filtered_attributes: data["attributes"] = filtered_attributes if relationships: self._validate_relationships(relationships) data["relationships"] = self._formatted_relationships(relationships) if links: self._validate_links(links) data["links"] = self._make_links(links) if meta_attributes: data["meta"] = meta_attributes return data def dump( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Dict] = None, relationships: Optional[Dict] = None, dump_function: Callable[[Dict], str] = json.dumps, ) -> str: """Call `jsonapi_dict()` method and dump the result with `dump_function`. ###### Parameters ###### For a documentation about the `required_attributes`, `links`, `relationships` parameters, see `BaseResource.jsonapi_dict()` method. - `dump_function`: a function used to dump the dictionary returned by `jsonapi_dict()` method. By default, the dump function is `json.dumps()`. ###### Returned value ###### A string dump of the JSON:API-compliant dictionary export of the object. ###### Error raised ###### See `BaseResource.jsonapi_dict()`. """ return dump_function( self.jsonapi_dict(required_attributes, links, relationships) ) @classmethod def register_link_factory(cls, name: str, factory: Callable[..., str]): """Add a link factory to the resource. When the resources are dump, these factories are used to produce their links. The factories are stored in the `__links_factories__` class attribute. ###### Parameters ###### - `name`: the name of the link (for example `"self"`) - `factory`: a callable taking the id of the resource as parameter and returning a string representing the url to dump. To register a link name of a relationship, prefix the link name with the attribute name and two underscores. For example, if an `"article"` resource has an `"author"` relationship attribute, you can register `"author__self"` or `"author__related"` links. ###### Returned value ###### `None` ###### Errors raised ###### If the relationship does not exist, raise a `ValueError`. """ _validate_link_name(cls, name) cls.__links_factories__[name] = factory ########################################################################### # # # P R I V A T E A P I # # ( U N D E R L Y I N G I M P L E M E N E N T A T I O N ) # # # ########################################################################### ########################################################################### # P R O P E R T I E S A N D U T I L S # ########################################################################### @property def _identifier_dict(self): identifier_dict = { "type": self.__resource_name__, "id": self.id, } if self.__identifier_meta_attributes__: identifier_dict["meta"] = {name: getattr(self, name) for name in self.__identifier_meta_attributes__} return identifier_dict @staticmethod def _qualname(name: str, relationship: Optional[str] = None): return f"{relationship}__{name}" if relationship else name ########################################################################### # V A L I D A T I O N # ########################################################################### @classmethod def _validate_relationships(cls, relationships: Dict) -> None: """Make sure that the provided `relationships` dictionary is valid - The keys must refer an existing relationships field. - The values must contain at least a data or a links member. Raise a `ValueError` if the dictionary is not valid. """ errors = [] for name, rel_dict in relationships.items(): if name not in cls.__relationships_fields_set__: errors.append(f" '{name}' is not a valid relationship.") if (rel_dict.get("links") is None and rel_dict.get("data")) is None: errors.append( f" You must provide at least links or data for the '{name}' relationship." ) if errors: raise ValueError("\n" + "\n".join(errors)) @classmethod def _validate_links(cls, links, relationship: Optional[str] = None): """Make sure that the links are registered in the resource class. Check if the passed names are keys of the __links_factories__ special attribute. If not, see if the links argument is a dictionary and try to get the value of the keys not present in __links_factories__. If at least one name is not valid, raise a `ValueError`. """ errors = [] for name in links: qual_name = cls._qualname(name, relationship) if qual_name in cls.__links_factories__: if not isinstance(links[name], Mapping): errors.append(f" You must provide an arguments dictionary for '{qual_name}' link.") continue provided_link = links.get(name) if provided_link is None: errors.append(f" Nothing provided for building '{qual_name}' link.") elif not isinstance(links[name], str): errors.append(f" Provided '{qual_name}' link is not a string.") if errors: raise ValueError("\n" + "\n".join(errors)) ########################################################################### # F O R M A T T I N G A N D F I L T E R I N G # ########################################################################### def _filtered_attributes( self, required_attributes: Union[Iterable, Literal["__all__"]] ) -> Tuple[Dict, Dict]: """Filter the attributes with provided `required_attributes` iterable. If a member of the iterable is not in the annotated attributes, raise a `ValueError`. The names are converted from snake case to camel case. """ if required_attributes == "__all__": required_attributes = self.__atomic_fields_set__ | {"meta"} required_attributes = set(required_attributes) errors = [] attrs = {name: getattr(self, name, None) for name in required_attributes-{"meta"}} for name in required_attributes - {"meta"}: if name not in self.__atomic_fields_set__: errors.append(f" Unexpected required attribute: '{name}'.") continue if attrs.get(name) is None: if not utils.is_an_optional_type_hint(self.__fields_types__[name]): errors.append(f" Missing required attribute: '{name}'.") if errors: raise ValueError("\n" + "\n".join(errors)) attrs = { utils.snake_to_camel_case(k): v for (k, v) in attrs.items() if k in set(required_attributes) - self._identifier_fields } meta_attrs = { utils.snake_to_camel_case(name): getattr(self, name) for name in self.__meta_attributes__ if getattr(self, name) is not None } if "meta" in required_attributes else None return attrs, meta_attrs def _formatted_relationships(self, relationships: Dict) -> Dict: """Format relationships into the JSON:API format.""" relationships_dict = {} for name, rel_payload in relationships.items(): rel_value: Union[Iterable[BaseResource], BaseResource] = self.__dict__[name] multiple_relationship = isinstance(rel_value, collections.abc.Iterable) if not rel_value: # None or empty relationships_dict[name] = [] if multiple_relationship else None continue relationship_links = rel_payload.get("links") data_is_required = rel_payload.get("data") rel_data = {} if relationship_links: self._validate_links(relationship_links, relationship=name) rel_data["links"] = self._make_links(relationship_links, relationship=name) relationships_dict[name] = rel_data if data_is_required: rel_data["data"] = ( [rel._identifier_dict for rel in rel_value] if multiple_relationship else rel_value._identifier_dict ) return relationships_dict def _make_links(self, links: Mapping[str, Union[str, Dict[str, Any]]], relationship: Optional[str] = None): """Build and return the links dictionary. The links are assumed to be valid. See _validate_links() for validation """ return { name: self.__links_factories__[self._qualname(name, relationship)](**links[name]) if self.__links_factories__.get(self._qualname(name, relationship)) is not None else links[name] for name in links } ########################################################################### # S P E C I A L M E T H O D S # ########################################################################### def __repr__(self): return (f"{self.__class__.__name__}" f"({', '.join(f'{k}={repr(v)}' for k, v in self.__dict__.items())})") def __getattr__(self, name): """Dynamically return None or [] for not-yet-initialized fields""" if name == "id": raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") type_hint = self.__fields_types__.get(name) if type_hint is None: raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") if utils.is_an_iterable_type_hint(type_hint): return [] return None def create_resource( name: str, meta_conf: Optional[Dict[str, Any]] = None, bases: Tuple[type] = (BaseResource,), metaklass: type = BaseResourceMeta, /, **fields_types ) -> Type[BaseResource]: """Create dynamically a new resource class. ###### Parameters ###### Positional: * `name`: The resource class name. * `meta_conf`: A dictionary containg configuration attributes (of the `Meta` inner class). * `bases`: A tuple containing parent classes. It must contain include `BaseResource`. * `metaklass`: The metaclass used to create the resource class (must be a subclass of `BaseResourceMeta`). Keywords: * `**fields_types`: The types of the fields as keyword arguments. ###### Returned value ###### A new resource class. ###### Errors raised ###### A `TypeError` is raised if `metaklass` is not a subclass of `BaseResourceMeta`. A `ValueError` is raised if `BaseResource` is not in `bases` argument. """ if not issubclass(metaklass, BaseResourceMeta): raise TypeError( "Only a submetaclass of BaseResourceMeta can create a new " f"resource class. ('{metaklass}' provided.)") if BaseResource not in bases: raise ValueError( "'BaseResource' class must be a parent class of any resource " "class.") meta_inncer_class = type("Meta", (), meta_conf or {}) namespace = {"__annotations__": fields_types, "Meta": meta_inncer_class} return metaklass(name, bases, namespace)
View Source
class BaseResource(metaclass=BaseResourceMeta): """Base class for defining resources. See the top of the `jsonapy.base` module for a doumentation on resource definition. This class is instantiated by the `BaseResourceMeta` metaclass. """ if TYPE_CHECKING: # for IDE, provided by metaclass __fields_types__: Dict[str, type] __atomic_fields_set__: Set[str] __relationships_fields_set__: Set[str] __resource_name__: str __is_abstract__: bool __identifier_meta_attributes__: Set[str] __links_factories__: Dict[str, Callable[..., str]] _identifier_fields: Set[str] _forbidden_fields: Set[str] # must be provided by subclasses id: Any class Meta: is_abstract: bool = True resource_name: str identifier_meta_attributes: Set[str] links_factories: Dict[str, Callable[..., str]] def __init__(self, **kwargs): """Automatically set all passed arguments. Take keyword arguments only and raise a `ValueError` if a parameter tries to reassign an already defined member (like the `jsonapi_dict()`. """ errors = [] for name in kwargs: if (hasattr(self, name) and name not in self.__fields_types__ or name in self._forbidden_fields): errors.append(f" This attribute name is reserved: '{name}'.") if errors: raise ValueError("\n" + "\n".join(errors)) for k, v in kwargs.items(): setattr(self, k, v) ########################################################################### # P U B L I C A P I # ########################################################################### def jsonapi_dict( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Mapping[str, Union[str, Mapping[str, Any]]]] = None, relationships: Optional[Dict] = None, ) -> Dict: """Export the object as a dictionary in compliance with JSON:API specification. ###### Parameters ###### - `required_attributes`: an iterable containing the fields names to include in dumped data. If all fields are required, provide the `"__all__"` literal instead of an iterable. - `links`: a dictionary containing links payload to include in data. The keys are the links names to dump, and the values are dictionaries containing keyword arguments to pass to the factory function. Raw strings can also be provided for unregistered links names. - `relationships`: a dictionary specifying the relationships to include, if the identifier object must be exported, and which links must be dumped. The keys must be the relationships names and the value of each key is another dict containing two keys: + `"data"`: a boolean indicating if an identifier object must be included. If there is identifier meta attributes, they are also exported. + `"links"`: a dictionary in the same shape as the `links` argument. ###### Returned value ###### A dictionary representing the object in compliance with the JSON:API specification. ###### Errors raised ###### A `ValueError` is raised if: - An attribute name in `required_attributes` argument is invalid. - A link name in `links` argument is not registered. - A key of the `relationship` argument does not refer to a valid related resource. - A link name of a specified relationship is not registered. ###### Examples ###### Given ```python aresource = AResource(id=1) bresource = BResource(id=1, name="foo", related=aresource) ``` A classic dump of `bresource` would be: ```python bresource.jsonapi_dict( required_attributes="__all__", links={"self": {"x": bresource.id}}, relationships={ "related":{ "data": True, "links": {"self": {"x": bresource.id}}}}) # {'type': 'BResource', # 'id': 1, # 'attributes': {'name': 'foo'}, # 'relationships': {'related': # {'links': {'self': '/b/1/relationships/related'}, # 'data': {'type': 'AResource', 'id': 1}}}, # 'links': {'self': '/b/1'}} ``` An export example with a raw string link: ```python aresource.jsonapi_dict( "__all__", {"self": f"/a/{aresource.id}"}) # {'type': 'AResource', 'id': 1, 'links': {'self': '/a/1'}} ``` """ if not hasattr(self, "id"): raise AttributeError(f"This '{self.__class__.__name__}' object has no id.") data = { "type": self.__resource_name__, "id": self.id, } filtered_attributes, meta_attributes = self._filtered_attributes(required_attributes) if filtered_attributes: data["attributes"] = filtered_attributes if relationships: self._validate_relationships(relationships) data["relationships"] = self._formatted_relationships(relationships) if links: self._validate_links(links) data["links"] = self._make_links(links) if meta_attributes: data["meta"] = meta_attributes return data def dump( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Dict] = None, relationships: Optional[Dict] = None, dump_function: Callable[[Dict], str] = json.dumps, ) -> str: """Call `jsonapi_dict()` method and dump the result with `dump_function`. ###### Parameters ###### For a documentation about the `required_attributes`, `links`, `relationships` parameters, see `BaseResource.jsonapi_dict()` method. - `dump_function`: a function used to dump the dictionary returned by `jsonapi_dict()` method. By default, the dump function is `json.dumps()`. ###### Returned value ###### A string dump of the JSON:API-compliant dictionary export of the object. ###### Error raised ###### See `BaseResource.jsonapi_dict()`. """ return dump_function( self.jsonapi_dict(required_attributes, links, relationships) ) @classmethod def register_link_factory(cls, name: str, factory: Callable[..., str]): """Add a link factory to the resource. When the resources are dump, these factories are used to produce their links. The factories are stored in the `__links_factories__` class attribute. ###### Parameters ###### - `name`: the name of the link (for example `"self"`) - `factory`: a callable taking the id of the resource as parameter and returning a string representing the url to dump. To register a link name of a relationship, prefix the link name with the attribute name and two underscores. For example, if an `"article"` resource has an `"author"` relationship attribute, you can register `"author__self"` or `"author__related"` links. ###### Returned value ###### `None` ###### Errors raised ###### If the relationship does not exist, raise a `ValueError`. """ _validate_link_name(cls, name) cls.__links_factories__[name] = factory ########################################################################### # # # P R I V A T E A P I # # ( U N D E R L Y I N G I M P L E M E N E N T A T I O N ) # # # ########################################################################### ########################################################################### # P R O P E R T I E S A N D U T I L S # ########################################################################### @property def _identifier_dict(self): identifier_dict = { "type": self.__resource_name__, "id": self.id, } if self.__identifier_meta_attributes__: identifier_dict["meta"] = {name: getattr(self, name) for name in self.__identifier_meta_attributes__} return identifier_dict @staticmethod def _qualname(name: str, relationship: Optional[str] = None): return f"{relationship}__{name}" if relationship else name ########################################################################### # V A L I D A T I O N # ########################################################################### @classmethod def _validate_relationships(cls, relationships: Dict) -> None: """Make sure that the provided `relationships` dictionary is valid - The keys must refer an existing relationships field. - The values must contain at least a data or a links member. Raise a `ValueError` if the dictionary is not valid. """ errors = [] for name, rel_dict in relationships.items(): if name not in cls.__relationships_fields_set__: errors.append(f" '{name}' is not a valid relationship.") if (rel_dict.get("links") is None and rel_dict.get("data")) is None: errors.append( f" You must provide at least links or data for the '{name}' relationship." ) if errors: raise ValueError("\n" + "\n".join(errors)) @classmethod def _validate_links(cls, links, relationship: Optional[str] = None): """Make sure that the links are registered in the resource class. Check if the passed names are keys of the __links_factories__ special attribute. If not, see if the links argument is a dictionary and try to get the value of the keys not present in __links_factories__. If at least one name is not valid, raise a `ValueError`. """ errors = [] for name in links: qual_name = cls._qualname(name, relationship) if qual_name in cls.__links_factories__: if not isinstance(links[name], Mapping): errors.append(f" You must provide an arguments dictionary for '{qual_name}' link.") continue provided_link = links.get(name) if provided_link is None: errors.append(f" Nothing provided for building '{qual_name}' link.") elif not isinstance(links[name], str): errors.append(f" Provided '{qual_name}' link is not a string.") if errors: raise ValueError("\n" + "\n".join(errors)) ########################################################################### # F O R M A T T I N G A N D F I L T E R I N G # ########################################################################### def _filtered_attributes( self, required_attributes: Union[Iterable, Literal["__all__"]] ) -> Tuple[Dict, Dict]: """Filter the attributes with provided `required_attributes` iterable. If a member of the iterable is not in the annotated attributes, raise a `ValueError`. The names are converted from snake case to camel case. """ if required_attributes == "__all__": required_attributes = self.__atomic_fields_set__ | {"meta"} required_attributes = set(required_attributes) errors = [] attrs = {name: getattr(self, name, None) for name in required_attributes-{"meta"}} for name in required_attributes - {"meta"}: if name not in self.__atomic_fields_set__: errors.append(f" Unexpected required attribute: '{name}'.") continue if attrs.get(name) is None: if not utils.is_an_optional_type_hint(self.__fields_types__[name]): errors.append(f" Missing required attribute: '{name}'.") if errors: raise ValueError("\n" + "\n".join(errors)) attrs = { utils.snake_to_camel_case(k): v for (k, v) in attrs.items() if k in set(required_attributes) - self._identifier_fields } meta_attrs = { utils.snake_to_camel_case(name): getattr(self, name) for name in self.__meta_attributes__ if getattr(self, name) is not None } if "meta" in required_attributes else None return attrs, meta_attrs def _formatted_relationships(self, relationships: Dict) -> Dict: """Format relationships into the JSON:API format.""" relationships_dict = {} for name, rel_payload in relationships.items(): rel_value: Union[Iterable[BaseResource], BaseResource] = self.__dict__[name] multiple_relationship = isinstance(rel_value, collections.abc.Iterable) if not rel_value: # None or empty relationships_dict[name] = [] if multiple_relationship else None continue relationship_links = rel_payload.get("links") data_is_required = rel_payload.get("data") rel_data = {} if relationship_links: self._validate_links(relationship_links, relationship=name) rel_data["links"] = self._make_links(relationship_links, relationship=name) relationships_dict[name] = rel_data if data_is_required: rel_data["data"] = ( [rel._identifier_dict for rel in rel_value] if multiple_relationship else rel_value._identifier_dict ) return relationships_dict def _make_links(self, links: Mapping[str, Union[str, Dict[str, Any]]], relationship: Optional[str] = None): """Build and return the links dictionary. The links are assumed to be valid. See _validate_links() for validation """ return { name: self.__links_factories__[self._qualname(name, relationship)](**links[name]) if self.__links_factories__.get(self._qualname(name, relationship)) is not None else links[name] for name in links } ########################################################################### # S P E C I A L M E T H O D S # ########################################################################### def __repr__(self): return (f"{self.__class__.__name__}" f"({', '.join(f'{k}={repr(v)}' for k, v in self.__dict__.items())})") def __getattr__(self, name): """Dynamically return None or [] for not-yet-initialized fields""" if name == "id": raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") type_hint = self.__fields_types__.get(name) if type_hint is None: raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'") if utils.is_an_iterable_type_hint(type_hint): return [] return None
Base class for defining resources.
See the top of the jsonapy.base
module for a doumentation on resource
definition.
This class is instantiated by the BaseResourceMeta
metaclass.
View Source
def __init__(self, **kwargs): """Automatically set all passed arguments. Take keyword arguments only and raise a `ValueError` if a parameter tries to reassign an already defined member (like the `jsonapi_dict()`. """ errors = [] for name in kwargs: if (hasattr(self, name) and name not in self.__fields_types__ or name in self._forbidden_fields): errors.append(f" This attribute name is reserved: '{name}'.") if errors: raise ValueError("\n" + "\n".join(errors)) for k, v in kwargs.items(): setattr(self, k, v)
Automatically set all passed arguments.
Take keyword arguments only and raise a ValueError
if a parameter
tries to reassign an already defined member (like the jsonapi_dict()
.
View Source
def jsonapi_dict( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Mapping[str, Union[str, Mapping[str, Any]]]] = None, relationships: Optional[Dict] = None, ) -> Dict: """Export the object as a dictionary in compliance with JSON:API specification. ###### Parameters ###### - `required_attributes`: an iterable containing the fields names to include in dumped data. If all fields are required, provide the `"__all__"` literal instead of an iterable. - `links`: a dictionary containing links payload to include in data. The keys are the links names to dump, and the values are dictionaries containing keyword arguments to pass to the factory function. Raw strings can also be provided for unregistered links names. - `relationships`: a dictionary specifying the relationships to include, if the identifier object must be exported, and which links must be dumped. The keys must be the relationships names and the value of each key is another dict containing two keys: + `"data"`: a boolean indicating if an identifier object must be included. If there is identifier meta attributes, they are also exported. + `"links"`: a dictionary in the same shape as the `links` argument. ###### Returned value ###### A dictionary representing the object in compliance with the JSON:API specification. ###### Errors raised ###### A `ValueError` is raised if: - An attribute name in `required_attributes` argument is invalid. - A link name in `links` argument is not registered. - A key of the `relationship` argument does not refer to a valid related resource. - A link name of a specified relationship is not registered. ###### Examples ###### Given ```python aresource = AResource(id=1) bresource = BResource(id=1, name="foo", related=aresource) ``` A classic dump of `bresource` would be: ```python bresource.jsonapi_dict( required_attributes="__all__", links={"self": {"x": bresource.id}}, relationships={ "related":{ "data": True, "links": {"self": {"x": bresource.id}}}}) # {'type': 'BResource', # 'id': 1, # 'attributes': {'name': 'foo'}, # 'relationships': {'related': # {'links': {'self': '/b/1/relationships/related'}, # 'data': {'type': 'AResource', 'id': 1}}}, # 'links': {'self': '/b/1'}} ``` An export example with a raw string link: ```python aresource.jsonapi_dict( "__all__", {"self": f"/a/{aresource.id}"}) # {'type': 'AResource', 'id': 1, 'links': {'self': '/a/1'}} ``` """ if not hasattr(self, "id"): raise AttributeError(f"This '{self.__class__.__name__}' object has no id.") data = { "type": self.__resource_name__, "id": self.id, } filtered_attributes, meta_attributes = self._filtered_attributes(required_attributes) if filtered_attributes: data["attributes"] = filtered_attributes if relationships: self._validate_relationships(relationships) data["relationships"] = self._formatted_relationships(relationships) if links: self._validate_links(links) data["links"] = self._make_links(links) if meta_attributes: data["meta"] = meta_attributes return data
Export the object as a dictionary in compliance with JSON:API specification.
Parameters
required_attributes
: an iterable containing the fields names to include in dumped data. If all fields are required, provide the"__all__"
literal instead of an iterable.links
: a dictionary containing links payload to include in data. The keys are the links names to dump, and the values are dictionaries containing keyword arguments to pass to the factory function. Raw strings can also be provided for unregistered links names.relationships
: a dictionary specifying the relationships to include, if the identifier object must be exported, and which links must be dumped. The keys must be the relationships names and the value of each key is another dict containing two keys:"data"
: a boolean indicating if an identifier object must be included. If there is identifier meta attributes, they are also exported."links"
: a dictionary in the same shape as thelinks
argument.
Returned value
A dictionary representing the object in compliance with the JSON:API specification.
Errors raised
A ValueError
is raised if:
- An attribute name in
required_attributes
argument is invalid. - A link name in
links
argument is not registered. - A key of the
relationship
argument does not refer to a valid related resource. - A link name of a specified relationship is not registered.
Examples
Given
aresource = AResource(id=1)
bresource = BResource(id=1, name="foo", related=aresource)
A classic dump of bresource
would be:
bresource.jsonapi_dict(
required_attributes="__all__",
links={"self": {"x": bresource.id}},
relationships={
"related":{
"data": True,
"links": {"self": {"x": bresource.id}}}})
# {'type': 'BResource',
# 'id': 1,
# 'attributes': {'name': 'foo'},
# 'relationships': {'related':
# {'links': {'self': '/b/1/relationships/related'},
# 'data': {'type': 'AResource', 'id': 1}}},
# 'links': {'self': '/b/1'}}
An export example with a raw string link:
aresource.jsonapi_dict(
"__all__",
{"self": f"/a/{aresource.id}"})
# {'type': 'AResource', 'id': 1, 'links': {'self': '/a/1'}}
View Source
def dump( self, required_attributes: Union[Iterable[str], Literal["__all__"]], links: Optional[Dict] = None, relationships: Optional[Dict] = None, dump_function: Callable[[Dict], str] = json.dumps, ) -> str: """Call `jsonapi_dict()` method and dump the result with `dump_function`. ###### Parameters ###### For a documentation about the `required_attributes`, `links`, `relationships` parameters, see `BaseResource.jsonapi_dict()` method. - `dump_function`: a function used to dump the dictionary returned by `jsonapi_dict()` method. By default, the dump function is `json.dumps()`. ###### Returned value ###### A string dump of the JSON:API-compliant dictionary export of the object. ###### Error raised ###### See `BaseResource.jsonapi_dict()`. """ return dump_function( self.jsonapi_dict(required_attributes, links, relationships) )
Call jsonapi_dict()
method and dump the result with dump_function
.
Parameters
For a documentation about the required_attributes
, links
, relationships
parameters, see BaseResource.jsonapi_dict()
method.
dump_function
: a function used to dump the dictionary returned byjsonapi_dict()
method. By default, the dump function isjson.dumps()
.
Returned value
A string dump of the JSON:API-compliant dictionary export of the object.
Error raised
View Source
@classmethod def register_link_factory(cls, name: str, factory: Callable[..., str]): """Add a link factory to the resource. When the resources are dump, these factories are used to produce their links. The factories are stored in the `__links_factories__` class attribute. ###### Parameters ###### - `name`: the name of the link (for example `"self"`) - `factory`: a callable taking the id of the resource as parameter and returning a string representing the url to dump. To register a link name of a relationship, prefix the link name with the attribute name and two underscores. For example, if an `"article"` resource has an `"author"` relationship attribute, you can register `"author__self"` or `"author__related"` links. ###### Returned value ###### `None` ###### Errors raised ###### If the relationship does not exist, raise a `ValueError`. """ _validate_link_name(cls, name) cls.__links_factories__[name] = factory
Add a link factory to the resource.
When the resources are dump, these factories are used to produce their
links. The factories are stored in the __links_factories__
class
attribute.
Parameters
name
: the name of the link (for example"self"
)factory
: a callable taking the id of the resource as parameter and returning a string representing the url to dump.
To register a link name of a relationship, prefix the link name with
the attribute name and two underscores. For example, if an "article"
resource has an "author"
relationship attribute, you can register
"author__self"
or "author__related"
links.
Returned value
None
Errors raised
If the relationship does not exist, raise a ValueError
.
View Source
def create_resource( name: str, meta_conf: Optional[Dict[str, Any]] = None, bases: Tuple[type] = (BaseResource,), metaklass: type = BaseResourceMeta, /, **fields_types ) -> Type[BaseResource]: """Create dynamically a new resource class. ###### Parameters ###### Positional: * `name`: The resource class name. * `meta_conf`: A dictionary containg configuration attributes (of the `Meta` inner class). * `bases`: A tuple containing parent classes. It must contain include `BaseResource`. * `metaklass`: The metaclass used to create the resource class (must be a subclass of `BaseResourceMeta`). Keywords: * `**fields_types`: The types of the fields as keyword arguments. ###### Returned value ###### A new resource class. ###### Errors raised ###### A `TypeError` is raised if `metaklass` is not a subclass of `BaseResourceMeta`. A `ValueError` is raised if `BaseResource` is not in `bases` argument. """ if not issubclass(metaklass, BaseResourceMeta): raise TypeError( "Only a submetaclass of BaseResourceMeta can create a new " f"resource class. ('{metaklass}' provided.)") if BaseResource not in bases: raise ValueError( "'BaseResource' class must be a parent class of any resource " "class.") meta_inncer_class = type("Meta", (), meta_conf or {}) namespace = {"__annotations__": fields_types, "Meta": meta_inncer_class} return metaklass(name, bases, namespace)
Create dynamically a new resource class.
Parameters
Positional:
name
: The resource class name.meta_conf
: A dictionary containg configuration attributes (of theMeta
inner class).bases
: A tuple containing parent classes. It must contain includeBaseResource
.metaklass
: The metaclass used to create the resource class (must be a subclass ofBaseResourceMeta
).
Keywords:
**fields_types
: The types of the fields as keyword arguments.
Returned value
A new resource class.
Errors raised
A TypeError
is raised if metaklass
is not a subclass of BaseResourceMeta
.
A ValueError
is raised if BaseResource
is not in bases
argument.
View Source
class BaseResourceMeta(type): """Metaclass of resource classes. Instantiate a resource class and perform some initialization and checks. ###### Initialization ###### The following class attributes are initialized here: - `__fields_types__`: an alias for `__annotations__` - `__atomic_fields_set__`: a set containing the names of fields whose type is not a instance of `BaseResourceMeta` (the names of the non resource fields). - `__relationships_fields_set__`: a set containing the names of the fields referring another resource. ###### Extraction of `Meta` attributes ###### This metaclass converts the `Meta` inner class of the defined models into special attributes: - `__is_abstract__`: the `Meta.is_abstract` attribute if defined, else `False`. - `__resource_name__`: the `Meta.resource_name` attribute if defined, else the name of the resource class. - `__identifier_meta_attributes__`: the `Meta.identifier_meta_attributes` attribute if defined, else an empty set. - `___meta_attributes__`: the `Meta.meta_attributes` attribute if defined, else an empty set. - `__links_factories__`: the `Meta.links_factories` attribute if defined, else an empty dictionary. If a link name is not valid, a `ValueError` is raised. """ def __new__(mcs, name, bases, namespace): try: meta = namespace.pop("Meta").__dict__ except KeyError: meta = {} cls = super().__new__(mcs, name, bases, namespace) # forbidden fields, see https://jsonapi.org/format/#document-resource-object-fields forbidden_fields = {"type", "links", "relationships"} # identifier fields, see https://jsonapi.org/format/#document-resource-identifier-objects identifier_fields = {"type", "id"} cls.__annotations__ = { name: type_ for klass in cls.mro() if isinstance(klass, BaseResourceMeta) for (name, type_) in klass.__annotations__.items() } # members special attributes annotations_items = cls.__annotations__.items() cls.__fields_types__ = cls.__annotations__ cls.__atomic_fields_set__ = { name for name, type_ in annotations_items if not utils.is_type_hint_instance_of(type_, mcs) } - forbidden_fields cls.__relationships_fields_set__ = { name for name, type_ in annotations_items if utils.is_type_hint_instance_of(type_, mcs) } links_factories = {} for name, factory in meta.get("links_factories", {}).items(): _validate_link_name(cls, name) links_factories[name] = factory # meta special attributes cls.__links_factories__ = links_factories cls.__is_abstract__ = meta.get("is_abstract", False) cls.__resource_name__ = meta.get("resource_name", cls.__name__) cls.__identifier_meta_attributes__ = set(meta.get("identifier_meta_attributes", set())) cls.__meta_attributes__ = set(meta.get("meta_attributes", set())) if not cls.__is_abstract__ and "id" not in cls.__annotations__: raise AttributeError("A Resource must have an 'id' attribute.") # utilitarian private attributes cls._forbidden_fields = forbidden_fields cls._identifier_fields = identifier_fields return cls
Metaclass of resource classes.
Instantiate a resource class and perform some initialization and checks.
Initialization
The following class attributes are initialized here:
__fields_types__
: an alias for__annotations__
__atomic_fields_set__
: a set containing the names of fields whose type is not a instance ofBaseResourceMeta
(the names of the non resource fields).__relationships_fields_set__
: a set containing the names of the fields referring another resource.
Extraction of Meta
attributes
This metaclass converts the Meta
inner class of the defined models into
special attributes:
__is_abstract__
: theMeta.is_abstract
attribute if defined, elseFalse
.__resource_name__
: theMeta.resource_name
attribute if defined, else the name of the resource class.__identifier_meta_attributes__
: theMeta.identifier_meta_attributes
attribute if defined, else an empty set.___meta_attributes__
: theMeta.meta_attributes
attribute if defined, else an empty set.__links_factories__
: theMeta.links_factories
attribute if defined, else an empty dictionary. If a link name is not valid, aValueError
is raised.
Inherited Members
- builtins.type
- type
- mro