Merge pull request #5824 from ynput/enhancement/OP-7071_Validate-Attributes

This commit is contained in:
Milan Kolar 2023-11-06 08:21:56 +01:00 committed by GitHub
commit 20eaa0b83e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 222 additions and 3 deletions

View file

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
"""Validator for Attributes."""
from pyblish.api import ContextPlugin, ValidatorOrder
from pymxs import runtime as rt
from openpype.pipeline.publish import (
OptionalPyblishPluginMixin,
PublishValidationError,
RepairContextAction
)
def has_property(object_name, property_name):
"""Return whether an object has a property with given name"""
return rt.Execute(f'isProperty {object_name} "{property_name}"')
def is_matching_value(object_name, property_name, value):
"""Return whether an existing property matches value `value"""
property_value = rt.Execute(f"{object_name}.{property_name}")
# Wrap property value if value is a string valued attributes
# starting with a `#`
if (
isinstance(value, str) and
value.startswith("#") and
not value.endswith(")")
):
# prefix value with `#`
# not applicable for #() array value type
# and only applicable for enum i.e. #bob, #sally
property_value = f"#{property_value}"
return property_value == value
class ValidateAttributes(OptionalPyblishPluginMixin,
ContextPlugin):
"""Validates attributes in the project setting are consistent
with the nodes from MaxWrapper Class in 3ds max.
E.g. "renderers.current.separateAovFiles",
"renderers.production.PrimaryGIEngine"
Admin(s) need to put the dict below and enable this validator for a check:
{
"renderers.current":{
"separateAovFiles" : True
},
"renderers.production":{
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE"
}
....
}
"""
order = ValidatorOrder
hosts = ["max"]
label = "Attributes"
actions = [RepairContextAction]
optional = True
@classmethod
def get_invalid(cls, context):
attributes = (
context.data["project_settings"]["max"]["publish"]
["ValidateAttributes"]["attributes"]
)
if not attributes:
return
invalid = []
for object_name, required_properties in attributes.items():
if not rt.Execute(f"isValidValue {object_name}"):
# Skip checking if the node does not
# exist in MaxWrapper Class
cls.log.debug(f"Unable to find '{object_name}'."
" Skipping validation of attributes.")
continue
for property_name, value in required_properties.items():
if not has_property(object_name, property_name):
cls.log.error(
"Non-existing property: "
f"{object_name}.{property_name}")
invalid.append((object_name, property_name))
if not is_matching_value(object_name, property_name, value):
cls.log.error(
f"Invalid value for: {object_name}.{property_name}"
f" should be: {value}")
invalid.append((object_name, property_name))
return invalid
def process(self, context):
if not self.is_active(context.data):
self.log.debug("Skipping Validate Attributes...")
return
invalid_attributes = self.get_invalid(context)
if invalid_attributes:
bullet_point_invalid_statement = "\n".join(
"- {}".format(invalid) for invalid
in invalid_attributes
)
report = (
"Required Attribute(s) have invalid value(s).\n\n"
f"{bullet_point_invalid_statement}\n\n"
"You can use repair action to fix them if they are not\n"
"unknown property value(s)."
)
raise PublishValidationError(
report, title="Invalid Value(s) for Required Attribute(s)")
@classmethod
def repair(cls, context):
attributes = (
context.data["project_settings"]["max"]["publish"]
["ValidateAttributes"]["attributes"]
)
invalid_attributes = cls.get_invalid(context)
for attrs in invalid_attributes:
prop, attr = attrs
value = attributes[prop][attr]
if isinstance(value, str) and not value.startswith("#"):
attribute_fix = '{}.{}="{}"'.format(
prop, attr, value
)
else:
attribute_fix = "{}.{}={}".format(
prop, attr, value
)
rt.Execute(attribute_fix)

View file

@ -639,6 +639,15 @@ def _convert_3dsmax_project_settings(ayon_settings, output):
for item in point_cloud_attribute
}
ayon_max["PointCloud"]["attribute"] = new_point_cloud_attribute
# --- Publish (START) ---
ayon_publish = ayon_max["publish"]
try:
attributes = json.loads(
ayon_publish["ValidateAttributes"]["attributes"]
)
except ValueError:
attributes = {}
ayon_publish["ValidateAttributes"]["attributes"] = attributes
output["max"] = ayon_max

View file

@ -36,6 +36,10 @@
"enabled": true,
"optional": true,
"active": true
},
"ValidateAttributes": {
"enabled": false,
"attributes": {}
}
}
}

View file

@ -28,6 +28,25 @@
"label": "Active"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "ValidateAttributes",
"label": "ValidateAttributes",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "raw-json",
"key": "attributes",
"label": "Attributes"
}
]
}
]
}

View file

@ -1,6 +1,30 @@
from pydantic import Field
import json
from pydantic import Field, validator
from ayon_server.settings import BaseSettingsModel
from ayon_server.exceptions import BadRequestException
class ValidateAttributesModel(BaseSettingsModel):
enabled: bool = Field(title="ValidateAttributes")
attributes: str = Field(
"{}", title="Attributes", widget="textarea")
@validator("attributes")
def validate_json(cls, value):
if not value.strip():
return "{}"
try:
converted_value = json.loads(value)
success = isinstance(converted_value, dict)
except json.JSONDecodeError:
success = False
if not success:
raise BadRequestException(
"The attibutes can't be parsed as json object"
)
return value
class BasicValidateModel(BaseSettingsModel):
@ -15,6 +39,10 @@ class PublishersModel(BaseSettingsModel):
title="Validate Frame Range",
section="Validators"
)
ValidateAttributes: ValidateAttributesModel = Field(
default_factory=ValidateAttributesModel,
title="Validate Attributes"
)
DEFAULT_PUBLISH_SETTINGS = {
@ -22,5 +50,9 @@ DEFAULT_PUBLISH_SETTINGS = {
"enabled": True,
"optional": True,
"active": True
}
},
"ValidateAttributes": {
"enabled": False,
"attributes": "{}"
},
}

View file

@ -1 +1 @@
__version__ = "0.1.0"
__version__ = "0.1.1"

View file

@ -118,4 +118,28 @@ Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Ca
This part of documentation is still work in progress.
:::
## Validators
Current Openpype integration supports different validators such as Frame Range and Attributes.
Some validators are mandatory while some are optional and user can choose to enable them in the setting.
**Validate Frame Range**: Optional Validator for checking Frame Range
**Validate Attributes**: Optional Validator for checking if object properties' attributes are valid
in MaxWrapper Class.
:::note
Users can write the properties' attributes they want to check in dict format in the setting
before validation. The attributes are then to be converted into Maxscript and do a check.
E.g. ```renderers.current.separateAovFiles``` and ```renderers.current.PrimaryGIEngine```
User can put the attributes in the dict format below
```
{
"renderer.current":{
"separateAovFiles" : True
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE"
}
}
```
![Validate Attribute Setting](assets/3dsmax_validate_attributes.png)
:::
## ...to be added

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB