mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #593 from pypeclub/feature/schema_templates
Setting GUI has schema templates
This commit is contained in:
commit
9ef9a71fce
4 changed files with 299 additions and 22 deletions
|
|
@ -19,6 +19,7 @@
|
|||
- GUI schemas are huge json files, to be able to split whole configuration into multiple schema there's type `schema`
|
||||
- system configuration schemas are stored in `~/tools/settings/settings/gui_schemas/system_schema/` and project configurations in `~/tools/settings/settings/gui_schemas/projects_schema/`
|
||||
- each schema name is filename of json file except extension (without ".json")
|
||||
- if content is dictionary content will be used as `schema` else will be used as `schema_template`
|
||||
|
||||
### schema
|
||||
- can have only key `"children"` which is list of strings, each string should represent another schema (order matters) string represebts name of the schema
|
||||
|
|
@ -31,6 +32,87 @@
|
|||
}
|
||||
```
|
||||
|
||||
### schema_template
|
||||
- allows to define schema "templates" to not duplicate same content multiple times
|
||||
```javascript
|
||||
// EXAMPLE json file content (filename: example_template.json)
|
||||
[
|
||||
{
|
||||
"__default_values__": {
|
||||
"multipath_executables": true
|
||||
}
|
||||
}, {
|
||||
"type": "raw-json",
|
||||
"label": "{host_label} Environments",
|
||||
"key": "{host_name}_environments",
|
||||
"env_group_key": "{host_name}"
|
||||
}, {
|
||||
"type": "path-widget",
|
||||
"key": "{host_name}_executables",
|
||||
"label": "{host_label} - Full paths to executables",
|
||||
"multiplatform": "{multipath_executables}",
|
||||
"multipath": true
|
||||
}
|
||||
]
|
||||
```
|
||||
```javascript
|
||||
// EXAMPLE usage of the template in schema
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "schema_template_examples",
|
||||
"label": "Schema template examples",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
// filename of template (example_template.json)
|
||||
"name": "example_template",
|
||||
"template_data": {
|
||||
"host_label": "Maya 2019",
|
||||
"host_name": "maya_2019",
|
||||
"multipath_executables": false
|
||||
}
|
||||
}, {
|
||||
"type": "schema_template",
|
||||
"name": "example_template",
|
||||
"template_data": {
|
||||
"host_label": "Maya 2020",
|
||||
"host_name": "maya_2020"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- item in schema mush contain `"type"` and `"name"` keys but it is also expected that `"template_data"` will be entered too
|
||||
- all items in the list, except `__default_values__`, will replace `schema_template` item in schema
|
||||
- template may contain another template or schema
|
||||
- it is expected that schema template will have unfilled fields as in example
|
||||
- unfilled fields are allowed only in values of schema dictionary
|
||||
```javascript
|
||||
{
|
||||
...
|
||||
// Allowed
|
||||
"key": "{to_fill}"
|
||||
...
|
||||
// Not allowed
|
||||
"{to_fill}": "value"
|
||||
...
|
||||
}
|
||||
```
|
||||
- Unfilled fields can be also used for non string values, in that case value must contain only one key and value for fill must contain right type.
|
||||
```javascript
|
||||
{
|
||||
...
|
||||
// Allowed
|
||||
"multiplatform": "{executable_multiplatform}"
|
||||
...
|
||||
// Not allowed
|
||||
"multiplatform": "{executable_multiplatform}_enhanced_string"
|
||||
...
|
||||
}
|
||||
```
|
||||
- It is possible to define default values for unfilled fields to do so one of items in list must be dictionary with key `"__default_values__"` and value as dictionary with default key: values (as in example above).
|
||||
|
||||
|
||||
## Basic Dictionary inputs
|
||||
- these inputs wraps another inputs into {key: value} relation
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,28 @@
|
|||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "schema_template_exaples",
|
||||
"label": "Schema template examples",
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "example_template",
|
||||
"template_data": {
|
||||
"host_label": "Maya 2019",
|
||||
"host_name": "maya_2019",
|
||||
"multipath_executables": false
|
||||
}
|
||||
}, {
|
||||
"type": "schema_template",
|
||||
"name": "example_template",
|
||||
"template_data": {
|
||||
"host_label": "Maya 2020",
|
||||
"host_name": "maya_2020"
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
"key": "env_group_test",
|
||||
"label": "EnvGroup Test",
|
||||
"type": "dict",
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
[
|
||||
{
|
||||
"__default_values__": {
|
||||
"multipath_executables": true
|
||||
}
|
||||
}, {
|
||||
"type": "raw-json",
|
||||
"label": "{host_label} Environments",
|
||||
"key": "{host_name}_environments",
|
||||
"env_group_key": "{host_name}"
|
||||
}, {
|
||||
"type": "path-widget",
|
||||
"key": "{host_name}_executables",
|
||||
"label": "{host_label} - Full paths to executables",
|
||||
"multiplatform": "{multipath_executables}",
|
||||
"multipath": true
|
||||
}
|
||||
]
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import re
|
||||
import json
|
||||
import copy
|
||||
from pype.settings.lib import M_OVERRIDEN_KEY, M_ENVIRONMENT_KEY
|
||||
|
|
@ -15,6 +16,8 @@ METADATA_KEY = type("METADATA_KEY", (), {})()
|
|||
OVERRIDE_VERSION = 1
|
||||
CHILD_OFFSET = 15
|
||||
|
||||
key_pattern = re.compile(r"(\{.*?[^{0]*\})")
|
||||
|
||||
|
||||
def convert_gui_data_with_metadata(data, ignored_keys=None):
|
||||
if not data or not isinstance(data, dict):
|
||||
|
|
@ -96,7 +99,111 @@ def convert_overrides_to_gui_data(data, first=True):
|
|||
return output
|
||||
|
||||
|
||||
def _fill_inner_schemas(schema_data, schema_collection):
|
||||
def _fill_schema_template_data(
|
||||
template, template_data, required_keys=None, missing_keys=None
|
||||
):
|
||||
first = False
|
||||
if required_keys is None:
|
||||
first = True
|
||||
required_keys = set()
|
||||
missing_keys = set()
|
||||
|
||||
_template = []
|
||||
default_values = {}
|
||||
for item in template:
|
||||
if isinstance(item, dict) and "__default_values__" in item:
|
||||
default_values = item["__default_values__"]
|
||||
else:
|
||||
_template.append(item)
|
||||
template = _template
|
||||
|
||||
for key, value in default_values.items():
|
||||
if key not in template_data:
|
||||
template_data[key] = value
|
||||
|
||||
if not template:
|
||||
output = template
|
||||
|
||||
elif isinstance(template, list):
|
||||
output = []
|
||||
for item in template:
|
||||
output.append(_fill_schema_template_data(
|
||||
item, template_data, required_keys, missing_keys
|
||||
))
|
||||
|
||||
elif isinstance(template, dict):
|
||||
output = {}
|
||||
for key, value in template.items():
|
||||
output[key] = _fill_schema_template_data(
|
||||
value, template_data, required_keys, missing_keys
|
||||
)
|
||||
|
||||
elif isinstance(template, str):
|
||||
# TODO find much better way how to handle filling template data
|
||||
for replacement_string in key_pattern.findall(template):
|
||||
key = str(replacement_string[1:-1])
|
||||
required_keys.add(key)
|
||||
if key not in template_data:
|
||||
missing_keys.add(key)
|
||||
continue
|
||||
|
||||
value = template_data[key]
|
||||
if replacement_string == template:
|
||||
# Replace the value with value from templates data
|
||||
# - with this is possible to set value with different type
|
||||
template = value
|
||||
else:
|
||||
# Only replace the key in string
|
||||
template = template.replace(replacement_string, value)
|
||||
output = template
|
||||
|
||||
else:
|
||||
output = template
|
||||
|
||||
if first and missing_keys:
|
||||
raise SchemaTemplateMissingKeys(missing_keys, required_keys)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def _fill_schema_template(child_data, schema_collection, schema_templates):
|
||||
template_name = child_data["name"]
|
||||
template = schema_templates.get(template_name)
|
||||
if template is None:
|
||||
if template_name in schema_collection:
|
||||
raise KeyError((
|
||||
"Schema \"{}\" is used as `schema_template`"
|
||||
).format(template_name))
|
||||
raise KeyError("Schema template \"{}\" was not found".format(
|
||||
template_name
|
||||
))
|
||||
|
||||
template_data = child_data.get("template_data") or {}
|
||||
try:
|
||||
filled_child = _fill_schema_template_data(
|
||||
template, template_data
|
||||
)
|
||||
|
||||
except SchemaTemplateMissingKeys as exc:
|
||||
raise SchemaTemplateMissingKeys(
|
||||
exc.missing_keys, exc.required_keys, template_name
|
||||
)
|
||||
|
||||
output = []
|
||||
for item in filled_child:
|
||||
filled_item = _fill_inner_schemas(
|
||||
item, schema_collection, schema_templates
|
||||
)
|
||||
if filled_item["type"] == "schema_template":
|
||||
output.extend(_fill_schema_template(
|
||||
filled_item, schema_collection, schema_templates
|
||||
))
|
||||
else:
|
||||
output.append(filled_item)
|
||||
return output
|
||||
|
||||
|
||||
def _fill_inner_schemas(schema_data, schema_collection, schema_templates):
|
||||
if schema_data["type"] == "schema":
|
||||
raise ValueError("First item in schema data can't be schema.")
|
||||
|
||||
|
|
@ -106,21 +213,62 @@ def _fill_inner_schemas(schema_data, schema_collection):
|
|||
|
||||
new_children = []
|
||||
for child in children:
|
||||
if child["type"] != "schema":
|
||||
new_child = _fill_inner_schemas(child, schema_collection)
|
||||
new_children.append(new_child)
|
||||
child_type = child["type"]
|
||||
if child_type == "schema":
|
||||
schema_name = child["name"]
|
||||
if schema_name not in schema_collection:
|
||||
if schema_name in schema_templates:
|
||||
raise KeyError((
|
||||
"Schema template \"{}\" is used as `schema`"
|
||||
).format(schema_name))
|
||||
raise KeyError(
|
||||
"Schema \"{}\" was not found".format(schema_name)
|
||||
)
|
||||
|
||||
filled_child = _fill_inner_schemas(
|
||||
schema_collection[schema_name],
|
||||
schema_collection,
|
||||
schema_templates
|
||||
)
|
||||
|
||||
elif child_type == "schema_template":
|
||||
for filled_child in _fill_schema_template(
|
||||
child, schema_collection, schema_templates
|
||||
):
|
||||
new_children.append(filled_child)
|
||||
continue
|
||||
|
||||
new_child = _fill_inner_schemas(
|
||||
schema_collection[child["name"]],
|
||||
schema_collection
|
||||
)
|
||||
new_children.append(new_child)
|
||||
else:
|
||||
filled_child = _fill_inner_schemas(
|
||||
child, schema_collection, schema_templates
|
||||
)
|
||||
|
||||
new_children.append(filled_child)
|
||||
|
||||
schema_data["children"] = new_children
|
||||
return schema_data
|
||||
|
||||
|
||||
class SchemaTemplateMissingKeys(Exception):
|
||||
def __init__(self, missing_keys, required_keys, template_name=None):
|
||||
self.missing_keys = missing_keys
|
||||
self.required_keys = required_keys
|
||||
if template_name:
|
||||
msg = f"Schema template \"{template_name}\" require more keys.\n"
|
||||
else:
|
||||
msg = ""
|
||||
msg += "Required keys: {}\nMissing keys: {}".format(
|
||||
self.join_keys(required_keys),
|
||||
self.join_keys(missing_keys)
|
||||
)
|
||||
super(SchemaTemplateMissingKeys, self).__init__(msg)
|
||||
|
||||
def join_keys(self, keys):
|
||||
return ", ".join([
|
||||
f"\"{key}\"" for key in keys
|
||||
])
|
||||
|
||||
|
||||
class SchemaMissingFileInfo(Exception):
|
||||
def __init__(self, invalid):
|
||||
full_path_keys = []
|
||||
|
|
@ -389,23 +537,30 @@ def gui_schema(subfolder, main_schema_name):
|
|||
)
|
||||
|
||||
loaded_schemas = {}
|
||||
for filename in os.listdir(dirpath):
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != ".json":
|
||||
continue
|
||||
loaded_schema_templates = {}
|
||||
for root, _, filenames in os.walk(dirpath):
|
||||
for filename in filenames:
|
||||
basename, ext = os.path.splitext(filename)
|
||||
if ext != ".json":
|
||||
continue
|
||||
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
with open(filepath, "r") as json_stream:
|
||||
try:
|
||||
schema_data = json.load(json_stream)
|
||||
except Exception as e:
|
||||
raise Exception((f"Unable to parse JSON file {json_stream}\n "
|
||||
f" - {e}")) from e
|
||||
loaded_schemas[basename] = schema_data
|
||||
filepath = os.path.join(root, filename)
|
||||
with open(filepath, "r") as json_stream:
|
||||
try:
|
||||
schema_data = json.load(json_stream)
|
||||
except Exception as exc:
|
||||
raise Exception((
|
||||
f"Unable to parse JSON file {filepath}\n{exc}"
|
||||
)) from exc
|
||||
if isinstance(schema_data, list):
|
||||
loaded_schema_templates[basename] = schema_data
|
||||
else:
|
||||
loaded_schemas[basename] = schema_data
|
||||
|
||||
main_schema = _fill_inner_schemas(
|
||||
loaded_schemas[main_schema_name],
|
||||
loaded_schemas
|
||||
loaded_schemas,
|
||||
loaded_schema_templates
|
||||
)
|
||||
validate_schema(main_schema)
|
||||
return main_schema
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue