Merge pull request #1161 from pypeclub/feature/sync_anatomy_modifications

Sync anatomy modifications
This commit is contained in:
Milan Kolar 2021-03-22 11:47:13 +01:00 committed by GitHub
commit 39433d71b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 337 additions and 174 deletions

View file

@ -1,6 +1,7 @@
import os
import collections
import copy
import json
import queue
import time
import datetime
@ -10,6 +11,7 @@ import traceback
from bson.objectid import ObjectId
from pymongo import UpdateOne
import arrow
import ftrack_api
from avalon import schema
@ -31,6 +33,15 @@ class SyncToAvalonEvent(BaseEvent):
ignore_ent_types = ["Milestone"]
ignore_keys = ["statusid", "thumbid"]
cust_attr_query_keys = [
"id",
"key",
"entity_type",
"object_type_id",
"is_hierarchical",
"config",
"default"
]
project_query = (
"select full_name, name, custom_attributes"
", project_schema._task_type_schema.types.name"
@ -115,10 +126,22 @@ class SyncToAvalonEvent(BaseEvent):
def avalon_cust_attrs(self):
if self._avalon_cust_attrs is None:
self._avalon_cust_attrs = avalon_sync.get_pype_attr(
self.process_session
self.process_session, query_keys=self.cust_attr_query_keys
)
return self._avalon_cust_attrs
@property
def cust_attr_types_by_id(self):
if self._cust_attr_types_by_id is None:
cust_attr_types = self.process_session.query(
"select id, name from CustomAttributeType"
).all()
self._cust_attr_types_by_id = {
cust_attr_type["id"]: cust_attr_type
for cust_attr_type in cust_attr_types
}
return self._cust_attr_types_by_id
@property
def avalon_entities(self):
if self._avalon_ents is None:
@ -227,15 +250,6 @@ class SyncToAvalonEvent(BaseEvent):
return self._changeability_by_mongo_id
@property
def avalon_custom_attributes(self):
"""Return info about changeability of entity and it's parents."""
if self._avalon_custom_attributes is None:
self._avalon_custom_attributes = avalon_sync.get_pype_attr(
self.process_session
)
return self._avalon_custom_attributes
def remove_cached_by_key(self, key, values):
if self._avalon_ents is None:
return
@ -380,6 +394,7 @@ class SyncToAvalonEvent(BaseEvent):
self._cur_project = None
self._avalon_cust_attrs = None
self._cust_attr_types_by_id = None
self._avalon_ents = None
self._avalon_ents_by_id = None
@ -391,7 +406,6 @@ class SyncToAvalonEvent(BaseEvent):
self._avalon_archived_by_id = None
self._avalon_archived_by_name = None
self._avalon_custom_attributes = None
self._ent_types_by_name = None
self.ftrack_ents_by_id = {}
@ -1234,54 +1248,28 @@ class SyncToAvalonEvent(BaseEvent):
return final_entity
def get_cust_attr_values(self, entity, keys=None):
def get_cust_attr_values(self, entity):
output = {}
custom_attrs, hier_attrs = self.avalon_custom_attributes
not_processed_keys = True
if keys:
not_processed_keys = [k for k in keys]
custom_attrs, hier_attrs = self.avalon_cust_attrs
# Notmal custom attributes
processed_keys = []
for attr in custom_attrs:
if not not_processed_keys:
break
key = attr["key"]
if key in processed_keys:
continue
if key in entity["custom_attributes"]:
output[key] = entity["custom_attributes"][key]
if key not in entity["custom_attributes"]:
continue
if keys:
if key not in keys:
continue
else:
not_processed_keys.remove(key)
output[key] = entity["custom_attributes"][key]
processed_keys.append(key)
if not not_processed_keys:
return output
# Hierarchical cust attrs
hier_keys = []
defaults = {}
for attr in hier_attrs:
key = attr["key"]
if keys and key not in keys:
continue
hier_keys.append(key)
defaults[key] = attr["default"]
hier_values = avalon_sync.get_hierarchical_attributes(
self.process_session, entity, hier_keys, defaults
hier_values = avalon_sync.get_hierarchical_attributes_values(
self.process_session,
entity,
hier_attrs,
self.cust_attr_types_by_id
)
for key, val in hier_values.items():
if key == CUST_ATTR_ID_KEY:
continue
output[key] = val
# Make sure mongo id is not set
output.pop(CUST_ATTR_ID_KEY, None)
return output
def process_renamed(self):
@ -1548,10 +1536,9 @@ class SyncToAvalonEvent(BaseEvent):
).format(entity_type, ent_info["entityType"]))
continue
_entity_key = collections.OrderedDict({
"configuration_id": mongo_id_configuration_id,
"entity_id": ftrack_id
})
_entity_key = collections.OrderedDict()
_entity_key["configuration_id"] = mongo_id_configuration_id
_entity_key["entity_id"] = ftrack_id
self.process_session.recorded_operations.push(
ftrack_api.operation.UpdateEntityOperation(
@ -1790,6 +1777,10 @@ class SyncToAvalonEvent(BaseEvent):
return
cust_attrs, hier_attrs = self.avalon_cust_attrs
hier_attrs_by_key = {
attr["key"]: attr
for attr in hier_attrs
}
cust_attrs_by_obj_id = collections.defaultdict(dict)
for cust_attr in cust_attrs:
key = cust_attr["key"]
@ -1805,8 +1796,6 @@ class SyncToAvalonEvent(BaseEvent):
obj_id = cust_attr["object_type_id"]
cust_attrs_by_obj_id[obj_id][key] = cust_attr
hier_attrs_keys = [attr["key"] for attr in hier_attrs]
for ftrack_id, ent_info in ent_infos.items():
mongo_id = ftrack_mongo_mapping[ftrack_id]
entType = ent_info["entityType"]
@ -1819,44 +1808,79 @@ class SyncToAvalonEvent(BaseEvent):
# Ftrack's entity_type does not have defined custom attributes
if ent_cust_attrs is None:
ent_cust_attrs = []
ent_cust_attrs = {}
for key, values in ent_info["changes"].items():
if key in hier_attrs_keys:
if key in hier_attrs_by_key:
self.hier_cust_attrs_changes[key].append(ftrack_id)
continue
if key not in ent_cust_attrs:
continue
value = values["new"]
new_value = self.convert_value_by_cust_attr_conf(
value, ent_cust_attrs[key]
)
if entType == "show" and key == "applications":
# Store apps to project't config
proj_apps, warnings = (
avalon_sync.get_project_apps(new_value)
)
if "config" not in self.updates[mongo_id]:
self.updates[mongo_id]["config"] = {}
self.updates[mongo_id]["config"]["apps"] = proj_apps
for msg, items in warnings.items():
if not msg or not items:
continue
self.report_items["warning"][msg] = items
continue
if "data" not in self.updates[mongo_id]:
self.updates[mongo_id]["data"] = {}
value = values["new"]
self.updates[mongo_id]["data"][key] = value
self.updates[mongo_id]["data"][key] = new_value
self.log.debug(
"Setting data value of \"{}\" to \"{}\" <{}>".format(
key, value, ent_path
key, new_value, ent_path
)
)
if entType != "show" or key != "applications":
continue
def convert_value_by_cust_attr_conf(self, value, cust_attr_conf):
type_id = cust_attr_conf["type_id"]
cust_attr_type_name = self.cust_attr_types_by_id[type_id]["name"]
ignored = (
"expression", "notificationtype", "dynamic enumerator"
)
if cust_attr_type_name in ignored:
return None
# Store apps to project't config
apps_str = ent_info["changes"]["applications"]["new"]
cust_attr_apps = [app for app in apps_str.split(", ") if app]
if cust_attr_type_name == "text":
return value
proj_apps, warnings = (
avalon_sync.get_project_apps(cust_attr_apps)
)
if "config" not in self.updates[mongo_id]:
self.updates[mongo_id]["config"] = {}
self.updates[mongo_id]["config"]["apps"] = proj_apps
if cust_attr_type_name == "boolean":
if value == "1":
return True
if value == "0":
return False
return bool(value)
for msg, items in warnings.items():
if not msg or not items:
continue
self.report_items["warning"][msg] = items
if cust_attr_type_name == "date":
return arrow.get(value)
cust_attr_config = json.loads(cust_attr_conf["config"])
if cust_attr_type_name == "number":
if cust_attr_config["isdecimal"]:
return float(value)
return int(value)
if cust_attr_type_name == "enumerator":
if not cust_attr_config["multiSelect"]:
return value
return value.split(", ")
return value
def process_hier_cleanup(self):
if (
@ -1968,7 +1992,7 @@ class SyncToAvalonEvent(BaseEvent):
self.update_entities()
return
cust_attrs, hier_attrs = self.avalon_cust_attrs
_, hier_attrs = self.avalon_cust_attrs
# Hierarchical custom attributes preparation ***
hier_attr_key_by_id = {
@ -2087,21 +2111,18 @@ class SyncToAvalonEvent(BaseEvent):
parent_queue.put(parent_ent)
# Prepare values to query
entity_ids_joined = ", ".join([
"\"{}\"".format(id) for id in cust_attrs_ftrack_ids
])
configuration_ids = set()
for key in hier_cust_attrs_keys:
configuration_ids.add(hier_attr_id_by_key[key])
attributes_joined = ", ".join([
"\"{}\"".format(conf_id) for conf_id in configuration_ids
])
entity_ids_joined = self.join_query_keys(cust_attrs_ftrack_ids)
attributes_joined = self.join_query_keys(configuration_ids)
queries = [{
"action": "query",
"expression": (
"select value, entity_id from CustomAttributeValue "
"select value, entity_id, configuration_id"
" from CustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
@ -2113,22 +2134,40 @@ class SyncToAvalonEvent(BaseEvent):
ftrack_project_id = self.cur_project["id"]
attr_types_by_id = self.cust_attr_types_by_id
convert_types_by_id = {}
for attr in hier_attrs:
key = attr["key"]
if key not in hier_cust_attrs_keys:
continue
type_id = attr["type_id"]
attr_id = attr["id"]
cust_attr_type_name = attr_types_by_id[type_id]["name"]
convert_type = avalon_sync.get_python_type_for_custom_attribute(
attr, cust_attr_type_name
)
convert_types_by_id[attr_id] = convert_type
entities_dict[ftrack_project_id]["hier_attrs"][key] = (
attr["default"]
)
# PREPARE DATA BEFORE THIS
avalon_hier = []
for value in values["data"]:
if value["value"] is None:
for item in values["data"]:
value = item["value"]
if value is None:
continue
entity_id = value["entity_id"]
key = hier_attr_key_by_id[value["configuration_id"]]
entities_dict[entity_id]["hier_attrs"][key] = value["value"]
entity_id = item["entity_id"]
configuration_id = item["configuration_id"]
convert_type = convert_types_by_id[configuration_id]
key = hier_attr_key_by_id[configuration_id]
if convert_type:
value = convert_type(value)
entities_dict[entity_id]["hier_attrs"][key] = value
# Get dictionary with not None hierarchical values to pull to childs
project_values = {}

View file

@ -83,15 +83,27 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None):
return False
def get_pype_attr(session, split_hierarchical=True):
def join_query_keys(keys):
return ",".join(["\"{}\"".format(key) for key in keys])
def get_pype_attr(session, split_hierarchical=True, query_keys=None):
custom_attributes = []
hier_custom_attributes = []
if not query_keys:
query_keys = [
"id",
"entity_type",
"object_type_id",
"is_hierarchical",
"default"
]
# TODO remove deprecated "avalon" group from query
cust_attrs_query = (
"select id, entity_type, object_type_id, is_hierarchical, default"
"select {}"
" from CustomAttributeConfiguration"
" where group.name in (\"avalon\", \"pype\")"
)
" where group.name in (\"avalon\", \"{}\")"
).format(", ".join(query_keys), CUST_ATTR_GROUP)
all_avalon_attr = session.query(cust_attrs_query).all()
for cust_attr in all_avalon_attr:
if split_hierarchical and cust_attr["is_hierarchical"]:
@ -107,6 +119,39 @@ def get_pype_attr(session, split_hierarchical=True):
return custom_attributes
def get_python_type_for_custom_attribute(cust_attr, cust_attr_type_name=None):
"""Python type that should value of custom attribute have.
This function is mainly for number type which is always float from ftrack.
Returns:
type: Python type which call be called on object to convert the object
to the type or None if can't figure out.
"""
if cust_attr_type_name is None:
cust_attr_type_name = cust_attr["type"]["name"]
if cust_attr_type_name == "text":
return str
if cust_attr_type_name == "boolean":
return bool
if cust_attr_type_name in ("number", "enumerator"):
cust_attr_config = json.loads(cust_attr["config"])
if cust_attr_type_name == "number":
if cust_attr_config["isdecimal"]:
return float
return int
if cust_attr_type_name == "enumerator":
if cust_attr_config["multiSelect"]:
return list
return str
# "date", "expression", "notificationtype", "dynamic enumerator"
return None
def from_dict_to_set(data, is_project):
"""
Converts 'data' into $set part of MongoDB update command.
@ -191,93 +236,97 @@ def get_project_apps(in_app_list):
apps = []
warnings = collections.defaultdict(list)
if not in_app_list:
return apps, warnings
missing_app_msg = "Missing definition of application"
application_manager = ApplicationManager()
for app_name in in_app_list:
app = application_manager.applications.get(app_name)
if app:
apps.append({
"name": app_name,
"label": app.full_label
})
if application_manager.applications.get(app_name):
apps.append({"name": app_name})
else:
warnings[missing_app_msg].append(app_name)
return apps, warnings
def get_hierarchical_attributes(session, entity, attr_names, attr_defaults={}):
entity_ids = []
if entity.entity_type.lower() == "project":
entity_ids.append(entity["id"])
else:
typed_context = session.query((
"select ancestors.id, project from TypedContext where id is \"{}\""
).format(entity["id"])).one()
entity_ids.append(typed_context["id"])
entity_ids.extend(
[ent["id"] for ent in reversed(typed_context["ancestors"])]
def get_hierarchical_attributes_values(
session, entity, hier_attrs, cust_attr_types=None
):
if not cust_attr_types:
cust_attr_types = session.query(
"select id, name from CustomAttributeType"
).all()
cust_attr_name_by_id = {
cust_attr_type["id"]: cust_attr_type["name"]
for cust_attr_type in cust_attr_types
}
# Hierarchical cust attrs
attr_key_by_id = {}
convert_types_by_attr_id = {}
defaults = {}
for attr in hier_attrs:
attr_id = attr["id"]
key = attr["key"]
type_id = attr["type_id"]
attr_key_by_id[attr_id] = key
defaults[key] = attr["default"]
cust_attr_type_name = cust_attr_name_by_id[type_id]
convert_type = get_python_type_for_custom_attribute(
attr, cust_attr_type_name
)
entity_ids.append(typed_context["project"]["id"])
convert_types_by_attr_id[attr_id] = convert_type
missing_defaults = []
for attr_name in attr_names:
if attr_name not in attr_defaults:
missing_defaults.append(attr_name)
entity_ids = [item["id"] for item in entity["link"]]
join_ent_ids = join_query_keys(entity_ids)
join_attribute_ids = join_query_keys(attr_key_by_id.keys())
join_ent_ids = ", ".join(
["\"{}\"".format(entity_id) for entity_id in entity_ids]
)
join_attribute_names = ", ".join(
["\"{}\"".format(key) for key in attr_names]
)
queries = []
queries.append({
"action": "query",
"expression": (
"select value, entity_id from CustomAttributeValue "
"where entity_id in ({}) and configuration.key in ({})"
).format(join_ent_ids, join_attribute_names)
"select value, configuration_id, entity_id"
" from CustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
).format(join_ent_ids, join_attribute_ids)
})
if not missing_defaults:
if hasattr(session, "call"):
[values] = session.call(queries)
else:
[values] = session._call(queries)
if hasattr(session, "call"):
[values] = session.call(queries)
else:
join_missing_names = ", ".join(
["\"{}\"".format(key) for key in missing_defaults]
)
queries.append({
"action": "query",
"expression": (
"select default from CustomAttributeConfiguration "
"where key in ({})"
).format(join_missing_names)
})
[values, default_values] = session.call(queries)
for default_value in default_values:
key = default_value["data"][0]["key"]
attr_defaults[key] = default_value["data"][0]["default"]
[values] = session._call(queries)
hier_values = {}
for key, val in attr_defaults.items():
for key, val in defaults.items():
hier_values[key] = val
if not values["data"]:
return hier_values
_hier_values = collections.defaultdict(list)
for value in values["data"]:
key = value["configuration"]["key"]
_hier_values[key].append(value)
values_by_entity_id = collections.defaultdict(dict)
for item in values["data"]:
value = item["value"]
if value is None:
continue
for key, values in _hier_values.items():
value = sorted(
values, key=lambda value: entity_ids.index(value["entity_id"])
)[0]
hier_values[key] = value["value"]
attr_id = item["configuration_id"]
convert_type = convert_types_by_attr_id[attr_id]
if convert_type:
value = convert_type(value)
key = attr_key_by_id[attr_id]
entity_id = item["entity_id"]
values_by_entity_id[entity_id][key] = value
for entity_id in entity_ids:
for key in attr_key_by_id.values():
value = values_by_entity_id[entity_id].get(key)
if value is not None:
hier_values[key] = value
return hier_values
@ -285,6 +334,16 @@ def get_hierarchical_attributes(session, entity, attr_names, attr_defaults={}):
class SyncEntitiesFactory:
dbcon = AvalonMongoDB()
cust_attr_query_keys = [
"id",
"key",
"entity_type",
"object_type_id",
"is_hierarchical",
"config",
"default"
]
project_query = (
"select full_name, name, custom_attributes"
", project_schema._task_type_schema.types.name"
@ -830,11 +889,21 @@ class SyncEntitiesFactory:
def set_cutom_attributes(self):
self.log.debug("* Preparing custom attributes")
# Get custom attributes and values
custom_attrs, hier_attrs = get_pype_attr(self.session)
custom_attrs, hier_attrs = get_pype_attr(
self.session, query_keys=self.cust_attr_query_keys
)
ent_types = self.session.query("select id, name from ObjectType").all()
ent_types_by_name = {
ent_type["name"]: ent_type["id"] for ent_type in ent_types
}
# Custom attribute types
cust_attr_types = self.session.query(
"select id, name from CustomAttributeType"
).all()
cust_attr_type_name_by_id = {
cust_attr_type["id"]: cust_attr_type["name"]
for cust_attr_type in cust_attr_types
}
# store default values per entity type
attrs_per_entity_type = collections.defaultdict(dict)
@ -844,9 +913,20 @@ class SyncEntitiesFactory:
avalon_attrs_ca_id = collections.defaultdict(dict)
attribute_key_by_id = {}
convert_types_by_attr_id = {}
for cust_attr in custom_attrs:
key = cust_attr["key"]
attribute_key_by_id[cust_attr["id"]] = key
attr_id = cust_attr["id"]
type_id = cust_attr["type_id"]
attribute_key_by_id[attr_id] = key
cust_attr_type_name = cust_attr_type_name_by_id[type_id]
convert_type = get_python_type_for_custom_attribute(
cust_attr, cust_attr_type_name
)
convert_types_by_attr_id[attr_id] = convert_type
ca_ent_type = cust_attr["entity_type"]
if key.startswith("avalon_"):
if ca_ent_type == "show":
@ -924,8 +1004,9 @@ class SyncEntitiesFactory:
])
cust_attr_query = (
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
"select value, configuration_id, entity_id"
" from ContextCustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
)
call_expr = [{
"action": "query",
@ -940,24 +1021,44 @@ class SyncEntitiesFactory:
for item in values["data"]:
entity_id = item["entity_id"]
key = attribute_key_by_id[item["configuration_id"]]
attr_id = item["configuration_id"]
key = attribute_key_by_id[attr_id]
store_key = "custom_attributes"
if key.startswith("avalon_"):
store_key = "avalon_attrs"
self.entities_dict[entity_id][store_key][key] = item["value"]
convert_type = convert_types_by_attr_id[attr_id]
value = item["value"]
if convert_type:
value = convert_type(value)
self.entities_dict[entity_id][store_key][key] = value
# process hierarchical attributes
self.set_hierarchical_attribute(hier_attrs, sync_ids)
self.set_hierarchical_attribute(
hier_attrs, sync_ids, cust_attr_type_name_by_id
)
def set_hierarchical_attribute(self, hier_attrs, sync_ids):
def set_hierarchical_attribute(
self, hier_attrs, sync_ids, cust_attr_type_name_by_id
):
# collect all hierarchical attribute keys
# and prepare default values to project
attributes_by_key = {}
attribute_key_by_id = {}
convert_types_by_attr_id = {}
for attr in hier_attrs:
key = attr["key"]
attribute_key_by_id[attr["id"]] = key
attr_id = attr["id"]
type_id = attr["type_id"]
attribute_key_by_id[attr_id] = key
attributes_by_key[key] = attr
cust_attr_type_name = cust_attr_type_name_by_id[type_id]
convert_type = get_python_type_for_custom_attribute(
attr, cust_attr_type_name
)
convert_types_by_attr_id[attr_id] = convert_type
self.hier_cust_attr_ids_by_key[key] = attr["id"]
store_key = "hier_attrs"
@ -992,7 +1093,7 @@ class SyncEntitiesFactory:
else:
prepare_dict[key] = None
for id, entity_dict in self.entities_dict.items():
for entity_dict in self.entities_dict.values():
# Skip project because has stored defaults at the moment
if entity_dict["entity_type"] == "project":
continue
@ -1011,8 +1112,9 @@ class SyncEntitiesFactory:
call_expr = [{
"action": "query",
"expression": (
"select value, entity_id from ContextCustomAttributeValue "
"where entity_id in ({}) and configuration_id in ({})"
"select value, entity_id, configuration_id"
" from ContextCustomAttributeValue"
" where entity_id in ({}) and configuration_id in ({})"
).format(entity_ids_joined, attributes_joined)
}]
if hasattr(self.session, "call"):
@ -1030,8 +1132,14 @@ class SyncEntitiesFactory:
or (isinstance(value, (tuple, list)) and not value)
):
continue
attr_id = item["configuration_id"]
convert_type = convert_types_by_attr_id[attr_id]
if convert_type:
value = convert_type(value)
entity_id = item["entity_id"]
key = attribute_key_by_id[item["configuration_id"]]
key = attribute_key_by_id[attr_id]
if key.startswith("avalon_"):
store_key = "avalon_attrs"
avalon_hier.append(key)
@ -1141,7 +1249,7 @@ class SyncEntitiesFactory:
proj_schema = entity["project_schema"]
task_types = proj_schema["_task_type_schema"]["types"]
proj_apps, warnings = get_project_apps(
(data.get("applications") or [])
data.pop("applications", [])
)
for msg, items in warnings.items():
if not msg or not items:
@ -1428,8 +1536,13 @@ class SyncEntitiesFactory:
old_parent_name = self.entities_dict[
self.ft_project_id]["name"]
else:
old_parent_name = self.avalon_ents_by_id[
ftrack_parent_mongo_id]["name"]
old_parent_name = "N/A"
if ftrack_parent_mongo_id in self.avalon_ents_by_id:
old_parent_name = (
self.avalon_ents_by_id
[ftrack_parent_mongo_id]
["name"]
)
self.updates[avalon_id]["data"] = {
"visualParent": new_parent_id
@ -2139,11 +2252,22 @@ class SyncEntitiesFactory:
final_doc_data = self.entities_dict[self.ft_project_id]["final_entity"]
final_doc_tasks = final_doc_data["config"].pop("tasks")
current_doc_tasks = self.avalon_project.get("config", {}).get("tasks")
# Update project's tasks if tasks are empty or are not same
if not final_doc_tasks:
# Update project's task types
if not current_doc_tasks:
update_tasks = True
else:
update_tasks = final_doc_tasks != current_doc_tasks
# Check if task types are same
update_tasks = False
for task_type in final_doc_tasks:
if task_type not in current_doc_tasks:
update_tasks = True
break
# Update new task types
# - but keep data about existing types and only add new one
if update_tasks:
for task_type, type_data in current_doc_tasks.items():
final_doc_tasks[task_type] = type_data
changes = self.compare_dict(final_doc_data, self.avalon_project)
@ -2372,7 +2496,7 @@ class SyncEntitiesFactory:
if new_entity_id not in p_chilren:
self.entities_dict[parent_id]["children"].append(new_entity_id)
cust_attr, hier_attrs = get_pype_attr(self.session)
cust_attr, _ = get_pype_attr(self.session)
for _attr in cust_attr:
key = _attr["key"]
if key not in av_entity["data"]: