Skip to content
4 changes: 4 additions & 0 deletions ayon_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
enroll_event_job,
get_attributes_schema,
reset_attributes_schema,
reset_attributes_cache,
set_attributes_cache_timeout,
set_attribute_config,
remove_attribute_config,
get_attributes_for_type,
Expand Down Expand Up @@ -433,6 +435,8 @@
"enroll_event_job",
"get_attributes_schema",
"reset_attributes_schema",
"reset_attributes_cache",
"set_attributes_cache_timeout",
"set_attribute_config",
"remove_attribute_config",
"get_attributes_for_type",
Expand Down
26 changes: 24 additions & 2 deletions ayon_api/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
EnrollEventData,
AttributeScope,
AttributeSchemaDataDict,
AttributeSchemaDict,
AttributesSchemaDict,
AddonsInfoDict,
InstallersInfoDict,
Expand Down Expand Up @@ -3582,10 +3581,30 @@ def get_attributes_schema(


def reset_attributes_schema() -> None:
"""Reset attributes schema cache.

DEPRECATED:
Use 'reset_attributes_cache' instead.

"""
con = get_server_api_connection()
return con.reset_attributes_schema()


def reset_attributes_cache() -> None:
con = get_server_api_connection()
return con.reset_attributes_cache()


def set_attributes_cache_timeout(
timeout: int,
) -> None:
con = get_server_api_connection()
return con.set_attributes_cache_timeout(
timeout=timeout,
)


def set_attribute_config(
attribute_name: str,
data: AttributeSchemaDataDict,
Expand Down Expand Up @@ -3622,7 +3641,7 @@ def remove_attribute_config(

def get_attributes_for_type(
entity_type: AttributeScope,
) -> dict[str, AttributeSchemaDict]:
) -> dict[str, AttributeSchemaDataDict]:
"""Get attribute schemas available for an entity type.

Example::
Expand Down Expand Up @@ -3670,6 +3689,9 @@ def get_attributes_fields_for_type(
) -> set[str]:
"""Prepare attribute fields for entity type.

DEPRECATED: Field 'attrib' is marked as deprecated and should not be
used for GraphQL queries.

Returns:
set[str]: Attributes fields for entity type.

Expand Down
129 changes: 99 additions & 30 deletions ayon_api/_api_helpers/attributes.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,107 @@
from __future__ import annotations

import copy
import time
import typing
from typing import Optional
import copy

from .base import BaseServerAPI

if typing.TYPE_CHECKING:
from ayon_api.typing import (
AttributeSchemaDataDict,
AttributeSchemaDict,
AttributeSchemaDataDict,
AttributesSchemaDict,
AttributeScope,
)

class _AttributesCache:
_schema = None
_last_fetch = 0
_timeout = 60
_attributes_by_type = {}

def reset_schema(self) -> None:
self._schema = None
self._last_fetch = 0
self._attributes_by_type = {}

def set_timeout(self, timeout: int) -> None:
self._timeout = timeout

def get_schema(self) -> AttributesSchemaDict:
return copy.deepcopy(self._schema)

def set_schema(self, schema: AttributesSchemaDict) -> None:
self._schema = schema
self._last_fetch = time.time()

def is_valid(self) -> bool:
if self._schema is None:
return False
return time.time() - self._last_fetch < self._timeout

def invalidate(self) -> None:
if not self.is_valid():
self.reset_schema()

def get_attributes_for_type(
self, entity_type: AttributeScope
) -> list[AttributeSchemaDict]:
attributes = self._attributes_by_type.get(entity_type)
if attributes is not None:
return attributes

attributes_schema = self.get_schema()
if attributes_schema is None:
raise ValueError("Attributes schema is not cached.")

attributes = []
for attr in attributes_schema["attributes"]:
if entity_type not in attr["scope"]:
continue
attributes.append(attr)

self._attributes_by_type[entity_type] = attributes
return attributes



class AttributesAPI(BaseServerAPI):
_attributes_schema = None
_entity_type_attributes_cache = {}
_attributes_cache = _AttributesCache()

def get_attributes_schema(
self, use_cache: bool = True
) -> AttributesSchemaDict:
if not use_cache:
self.reset_attributes_schema()
self._attributes_cache.reset_schema()
else:
self._attributes_cache.invalidate()

if self._attributes_schema is None:
if not self._attributes_cache.is_valid():
result = self.get("attributes")
result.raise_for_status()
self._attributes_schema = result.data
return copy.deepcopy(self._attributes_schema)
self._attributes_cache.set_schema(result.data)
return self._attributes_cache.get_schema()

def reset_attributes_schema(self) -> None:
self._attributes_schema = None
self._entity_type_attributes_cache = {}
"""Reset attributes schema cache.

DEPRECATED:
Use 'reset_attributes_cache' instead.

"""
self.log.warning(
"Used deprecated function 'reset_attributes_schema'."
" Please use 'reset_attributes_cache' instead."
)
self.reset_attributes_cache()

def reset_attributes_cache(self) -> None:
self._attributes_cache.reset_schema()

def set_attributes_cache_timeout(self, timeout: int) -> None:
self._attributes_cache.set_timeout(timeout)

def set_attribute_config(
self,
Expand Down Expand Up @@ -64,12 +132,10 @@ def set_attribute_config(
position=position,
builtin=builtin
)
if response.status_code != 204:
# TODO raise different exception
raise ValueError(
f"Attribute \"{attribute_name}\" was not created/updated."
f" {response.detail}"
)
response.raise_for_status(
f"Attribute \"{attribute_name}\" was not created/updated."
f" {response.detail}"
)

self.reset_attributes_schema()

Expand All @@ -92,7 +158,7 @@ def remove_attribute_config(self, attribute_name: str) -> None:

def get_attributes_for_type(
self, entity_type: AttributeScope
) -> dict[str, AttributeSchemaDict]:
) -> dict[str, AttributeSchemaDataDict]:
"""Get attribute schemas available for an entity type.

Example::
Expand Down Expand Up @@ -129,29 +195,32 @@ def get_attributes_for_type(
for entered entity type.

"""
attributes = self._entity_type_attributes_cache.get(entity_type)
if attributes is None:
attributes_schema = self.get_attributes_schema()
attributes = {}
for attr in attributes_schema["attributes"]:
if entity_type not in attr["scope"]:
continue
attr_name = attr["name"]
attributes[attr_name] = attr["data"]

self._entity_type_attributes_cache[entity_type] = attributes

return copy.deepcopy(attributes)
# Make sure attributes are cached
self.get_attributes_schema()
return {
attr["name"]: attr["data"]
for attr in self._attributes_cache.get_attributes_for_type(
entity_type
)
}

def get_attributes_fields_for_type(
self, entity_type: AttributeScope
) -> set[str]:
"""Prepare attribute fields for entity type.

DEPRECATED: Field 'attrib' is marked as deprecated and should not be
used for GraphQL queries.

Returns:
set[str]: Attributes fields for entity type.

"""
self.log.warning(
"Method 'get_attributes_fields_for_type' is deprecated and should"
" not be used for GraphQL queries. Use 'allAttrib' field instead"
" of 'attrib'."
)
attributes = self.get_attributes_for_type(entity_type)
return {
f"attrib.{attr}"
Expand Down
3 changes: 2 additions & 1 deletion ayon_api/_api_helpers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
ProjectDict,
StreamType,
AttributeScope,
AttributeSchemaDataDict,
)

_PLACEHOLDER = object()
Expand Down Expand Up @@ -134,7 +135,7 @@ def get_user(

def get_attributes_for_type(
self, entity_type: AttributeScope
) -> set[str]:
) -> dict[str, AttributeSchemaDataDict]:
raise NotImplementedError()

def get_attributes_fields_for_type(
Expand Down
31 changes: 14 additions & 17 deletions ayon_api/_api_helpers/lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ def get_entity_lists(
if fields is None:
fields = self.get_default_fields_for_type("entityList")

# List does not have 'attrib' field but has 'allAttrib' field
# which is json string and contains only values that are set
o_fields = tuple(fields)
fields = set()
requires_attrib = False
add_all_attrib = False
for field in o_fields:
if field == "attrib" or field.startswith("attrib."):
requires_attrib = True
field = "allAttrib"
fields.add(field)
add_all_attrib = True
else:
fields.add(field)

if add_all_attrib:
fields.add("allAttrib")

if "items" in fields:
fields.discard("items")
Expand All @@ -71,8 +72,8 @@ def get_entity_lists(
"items.position",
}

available_attribs = []
if requires_attrib:
available_attribs = {}
if "allAttrib" in fields:
available_attribs = self.get_attributes_for_type("list")

if active is not None:
Expand All @@ -97,17 +98,13 @@ def get_entity_lists(
if isinstance(attributes, str):
entity_list["attributes"] = json.loads(attributes)

if requires_attrib:
all_attrib = json.loads(
entity_list.get("allAttrib") or "{}"
)
entity_list["attrib"] = {
attrib_name: all_attrib.get(attrib_name)
for attrib_name in available_attribs
}

self._convert_entity_data(entity_list)

attrib = entity_list.get("attrib")
if attrib is not None:
for attrib_name, attrib_data in available_attribs.items():
attrib.setdefault(attrib_name, attrib_data["default"])

yield entity_list

def get_entity_list_rest(
Expand Down
Loading
Loading