#!/usr/bin/env python3
#
# Copyright 2021 Graviti. Licensed under MIT License.
#
"""Items and AttributeInfo.
:class:`AttributeInfo` represents the information of an attribute.
It refers to the `Json schema`_ method to describe an attribute.
:class:`Items` is the base class of :class:`AttributeInfo`, representing the items of an attribute.
.. _Json schema: https://json-schema.org/
"""
from enum import Enum
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union
from ..utility import EqMixin, NameMixin, ReprMixin, attr, attr_base, camel, common_loads
_AvailaleType = Union[list, bool, int, float, str, None]
_SingleArgType = Union[str, None, Type[_AvailaleType]]
_ArgType = Union[_SingleArgType, Iterable[_SingleArgType]]
_EnumElementType = Union[str, float, bool, None]
class _AttributeType(Enum):
"""All the possible type of the attributes."""
# pylint: disable=invalid-name
array = list
boolean = bool
integer = int
number = float
string = str
null = None
instance = "instance"
@classmethod
def get_type_name(cls, type_: _SingleArgType) -> str:
"""Return the corresponding enumeration type name of the given string or type.
Arguments:
type_: A string or type indicating the attribute type.
Returns:
The name of the :class:`_AttributeType` object corresponding to the given type_.
Raises:
ValueError: When the input type_ is not supported.
"""
if type_ in cls.__members__:
return type_ # type: ignore[return-value]
try:
return cls(type_).name
except ValueError as error:
raise ValueError(
"Invalid type_ values for attribute. "
f"Only support {tuple(cls.__members__.keys())} attribute types"
) from error
[docs]class Items(ReprMixin, EqMixin):
"""The base class of :class:`AttributeInfo`, representing the items of an attribute.
When the value type of an attribute is array,
the :class:`AttributeInfo` would contain an 'items' field.
.. todo::
The format of argument *type_* on the generated web page is incorrect.
Arguments:
type_: The type of the attribute value, could be a single type or multi-types.
The type must be within the followings:
- array
- boolean
- integer
- number
- string
- null
- instance
enum: All the possible values of an enumeration attribute.
minimum: The minimum value of number type attribute.
maximum: The maximum value of number type attribute.
items: The items inside array type attributes.
Attributes:
type: The type of the attribute value, could be a single type or multi-types.
enum: All the possible values of an enumeration attribute.
minimum: The minimum value of number type attribute.
maximum: The maximum value of number type attribute.
items: The items inside array type attributes.
Raises:
TypeError: When both ``enum`` and ``type_`` are absent or
when ``type_`` is array and ``items`` is absent.
Examples:
>>> Items(type_="integer", enum=[1, 2, 3, 4, 5], minimum=1, maximum=5)
Items(
(type): 'integer',
(enum): [...],
(minimum): 1,
(maximum): 5
)
"""
_T = TypeVar("_T", bound="Items")
_repr_attrs: Tuple[str, ...] = ("type", "enum", "minimum", "maximum", "items")
def __init__(
self,
*,
type_: _ArgType = "",
enum: Optional[Iterable[_EnumElementType]] = None,
minimum: Optional[float] = None,
maximum: Optional[float] = None,
items: Optional["Items"] = None,
):
if type_ != "":
self.type, has_array = self._convert_type(type_)
if has_array and items:
self.items = items
if enum:
self.enum = list(enum)
if minimum is not None:
self.minimum = minimum
if minimum is not None:
self.maximum = maximum
@staticmethod
def _convert_type(type_: _ArgType) -> Tuple[Union[str, List[str]], bool]:
if isinstance(type_, Iterable) and not isinstance(type_, str): # pylint: disable=W1116
converted_types = [_AttributeType.get_type_name(single_type) for single_type in type_]
return converted_types, "array" in converted_types
converted_type = _AttributeType.get_type_name(type_)
return converted_type, converted_type == "array"
def _loads(self, contents: Dict[str, Any]) -> None:
if "type" in contents:
self.type, has_array = self._convert_type(contents["type"])
if has_array:
self.items = Items.loads(contents["items"])
if "enum" in contents:
self.enum = contents["enum"]
if "minimum" in contents:
self.minimum = contents["minimum"]
if "maximum" in contents:
self.maximum = contents["maximum"]
[docs] @classmethod
def loads(cls: Type[_T], contents: Dict[str, Any]) -> _T:
"""Load an Items from a dict containing the items information.
Arguments:
contents: A dict containing the information of the items.
Returns:
The loaded :class:`Items` object.
Examples:
>>> contents = {
... "type": "array",
... "enum": [1, 2, 3, 4, 5],
... "minimum": 1,
... "maximum": 5,
... "items": {
... "enum": [None],
... "type": "null",
... },
... }
>>> Items.loads(contents)
Items(
(type): 'array',
(enum): [...],
(minimum): 1,
(maximum): 5,
(items): Items(...)
)
"""
return common_loads(cls, contents)
[docs] def dumps(self) -> Dict[str, Any]:
"""Dumps the information of the items into a dict.
Returns:
A dict containing all the information of the items.
Examples:
>>> items = Items(type_="integer", enum=[1, 2, 3, 4, 5], minimum=1, maximum=5)
>>> items.dumps()
{'type': 'integer', 'enum': [1, 2, 3, 4, 5], 'minimum': 1, 'maximum': 5}
"""
contents: Dict[str, Any] = {}
if hasattr(self, "type"):
contents["type"] = self.type
if hasattr(self, "items"):
contents["items"] = self.items.dumps()
if hasattr(self, "enum"):
contents["enum"] = self.enum
if hasattr(self, "minimum"):
contents["minimum"] = self.minimum
if hasattr(self, "maximum"):
contents["maximum"] = self.maximum
return contents
[docs]class AttributeInfo(NameMixin, Items):
"""This class represents the information of an attribute.
It refers to the `Json schema`_ method to describe an attribute.
.. todo::
The format of argument *type_* on the generated web page is incorrect.
Arguments:
name: The name of the attribute.
type_: The type of the attribute value, could be a single type or multi-types.
The type must be within the followings:
- array
- boolean
- integer
- number
- string
- null
- instance
enum: All the possible values of an enumeration attribute.
minimum: The minimum value of number type attribute.
maximum: The maximum value of number type attribute.
items: The items inside array type attributes.
parent_categories: The parent categories of the attribute.
description: The description of the attribute.
Attributes:
type: The type of the attribute value, could be a single type or multi-types.
enum: All the possible values of an enumeration attribute.
minimum: The minimum value of number type attribute.
maximum: The maximum value of number type attribute.
items: The items inside array type attributes.
parent_categories: The parent categories of the attribute.
description: The description of the attribute.
.. _Json schema: https://json-schema.org/
Examples:
>>> from tensorbay.label import Items
>>> items = Items(type_="integer", enum=[1, 2, 3, 4, 5], minimum=1, maximum=5)
>>> AttributeInfo(
... name="example",
... type_="array",
... enum=[1, 2, 3, 4, 5],
... items=items,
... minimum=1,
... maximum=5,
... parent_categories=["parent_category_of_example"],
... description="This is an example",
... )
AttributeInfo("example")(
(type): 'array',
(enum): [
1,
2,
3,
4,
5
],
(minimum): 1,
(maximum): 5,
(items): Items(
(type): 'integer',
(enum): [...],
(minimum): 1,
(maximum): 5
),
(parent_categories): [
'parent_category_of_example'
]
)
"""
_T = TypeVar("_T", bound="AttributeInfo")
_repr_attrs = Items._repr_attrs + ("parent_categories",)
_repr_maxlevel = 2
_attrs_base: Items = attr_base(key=None)
parent_categories: List[str] = attr(is_dynamic=True, key=camel)
def __init__(
self,
name: str,
*,
type_: _ArgType = "",
enum: Optional[Iterable[_EnumElementType]] = None,
minimum: Optional[float] = None,
maximum: Optional[float] = None,
items: Optional[Items] = None,
parent_categories: Union[None, str, Iterable[str]] = None,
description: str = "",
):
NameMixin.__init__(self, name, description)
Items.__init__(self, type_=type_, enum=enum, minimum=minimum, maximum=maximum, items=items)
if not parent_categories:
return
if isinstance(parent_categories, str):
self.parent_categories = [parent_categories]
else:
self.parent_categories = list(parent_categories)
[docs] @classmethod
def loads(cls: Type[_T], contents: Dict[str, Any]) -> _T:
"""Load an AttributeInfo from a dict containing the attribute information.
Arguments:
contents: A dict containing the information of the attribute.
Returns:
The loaded :class:`AttributeInfo` object.
Examples:
>>> contents = {
... "name": "example",
... "type": "array",
... "items": {"type": "boolean"},
... "description": "This is an example",
... "parentCategories": ["parent_category_of_example"],
... }
>>> AttributeInfo.loads(contents)
AttributeInfo("example")(
(type): 'array',
(items): Items(
(type): 'boolean',
),
(parent_categories): [
'parent_category_of_example'
]
)
"""
return common_loads(cls, contents)
[docs] def dumps(self) -> Dict[str, Any]:
"""Dumps the information of this attribute into a dict.
Returns:
A dict containing all the information of this attribute.
Examples:
>>> from tensorbay.label import Items
>>> items = Items(type_="integer", minimum=1, maximum=5)
>>> attributeinfo = AttributeInfo(
... name="example",
... type_="array",
... items=items,
... parent_categories=["parent_category_of_example"],
... description="This is an example",
... )
>>> attributeinfo.dumps()
{
'name': 'example',
'description': 'This is an example',
'type': 'array',
'items': {'type': 'integer', 'minimum': 1, 'maximum': 5},
'parentCategories': ['parent_category_of_example'],
}
"""
return self._dumps()