Merge branch 'develop' into bugfix/workfile_builder_on_start

This commit is contained in:
Mustafa Jafar 2024-10-02 12:27:53 +03:00 committed by GitHub
commit 8bd0c9ca07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 387 additions and 130 deletions

12
.github/workflows/release_trigger.yml vendored Normal file
View file

@ -0,0 +1,12 @@
name: 🚀 Release Trigger
on:
workflow_dispatch:
jobs:
call-release-trigger:
uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main
secrets:
token: ${{ secrets.YNPUT_BOT_TOKEN }}
email: ${{ secrets.CI_EMAIL }}
user: ${{ secrets.CI_USER }}

View file

@ -19,7 +19,8 @@ class OCIOEnvHook(PreLaunchHook):
"nuke",
"hiero",
"resolve",
"openrv"
"openrv",
"cinema4d"
}
launch_types = set()

View file

@ -81,7 +81,10 @@ def collect_frames(files):
dict: {'/folder/product_v001.0001.png': '0001', ....}
"""
patterns = [clique.PATTERNS["frames"]]
# clique.PATTERNS["frames"] supports only `.1001.exr` not `_1001.exr` so
# we use a customized pattern.
pattern = "[_.](?P<index>(?P<padding>0*)\\d+)\\.\\D+\\d?$"
patterns = [pattern]
collections, remainder = clique.assemble(
files, minimum_items=1, patterns=patterns)

View file

@ -3,7 +3,6 @@ from .constants import (
AVALON_INSTANCE_ID,
AYON_CONTAINER_ID,
AYON_INSTANCE_ID,
HOST_WORKFILE_EXTENSIONS,
)
from .anatomy import Anatomy
@ -114,7 +113,6 @@ __all__ = (
"AVALON_INSTANCE_ID",
"AYON_CONTAINER_ID",
"AYON_INSTANCE_ID",
"HOST_WORKFILE_EXTENSIONS",
# --- Anatomy ---
"Anatomy",

View file

@ -699,6 +699,34 @@ def get_ocio_config_views(config_path):
)
def _get_config_path_from_profile_data(
profile, profile_type, template_data
):
"""Get config path from profile data.
Args:
profile (dict[str, Any]): Profile data.
profile_type (str): Profile type.
template_data (dict[str, Any]): Template data.
Returns:
dict[str, str]: Config data with path and template.
"""
template = profile[profile_type]
result = StringTemplate.format_strict_template(
template, template_data
)
normalized_path = str(result.normalized())
if not os.path.exists(normalized_path):
log.warning(f"Path was not found '{normalized_path}'.")
return None
return {
"path": normalized_path,
"template": template
}
def _get_global_config_data(
project_name,
host_name,
@ -717,7 +745,7 @@ def _get_global_config_data(
2. Custom path to ocio config.
3. Path to 'ocioconfig' representation on product. Name of product can be
defined in settings. Product name can be regex but exact match is
always preferred.
always preferred. Fallback can be defined in case no product is found.
None is returned when no profile is found, when path
@ -755,30 +783,36 @@ def _get_global_config_data(
profile_type = profile["type"]
if profile_type in ("builtin_path", "custom_path"):
template = profile[profile_type]
result = StringTemplate.format_strict_template(
template, template_data
)
normalized_path = str(result.normalized())
if not os.path.exists(normalized_path):
log.warning(f"Path was not found '{normalized_path}'.")
return None
return {
"path": normalized_path,
"template": template
}
return _get_config_path_from_profile_data(
profile, profile_type, template_data)
# TODO decide if this is the right name for representation
repre_name = "ocioconfig"
published_product_data = profile["published_product"]
product_name = published_product_data["product_name"]
fallback_data = published_product_data["fallback"]
if product_name == "":
log.error(
"Colorspace OCIO config path cannot be set. "
"Profile is set to published product but `Product name` is empty."
)
return None
folder_info = template_data.get("folder")
if not folder_info:
log.warning("Folder info is missing.")
return None
log.info("Using fallback data for ocio config path.")
# in case no product was found we need to use fallback
fallback_type = fallback_data["fallback_type"]
return _get_config_path_from_profile_data(
fallback_data, fallback_type, template_data
)
folder_path = folder_info["path"]
product_name = profile["product_name"]
if folder_id is None:
folder_entity = ayon_api.get_folder_by_path(
project_name, folder_path, fields={"id"}
@ -797,12 +831,13 @@ def _get_global_config_data(
fields={"id", "name"}
)
}
if not product_entities_by_name:
log.debug(
f"No product entities were found for folder '{folder_path}' with"
f" product name filter '{product_name}'."
# in case no product was found we need to use fallback
fallback_type = fallback_data["type"]
return _get_config_path_from_profile_data(
fallback_data, fallback_type, template_data
)
return None
# Try to use exact match first, otherwise use first available product
product_entity = product_entities_by_name.get(product_name)
@ -837,6 +872,7 @@ def _get_global_config_data(
path = get_representation_path_with_anatomy(repre_entity, anatomy)
template = repre_entity["attrib"]["template"]
return {
"path": path,
"template": template,

View file

@ -4,20 +4,3 @@ AYON_INSTANCE_ID = "ayon.create.instance"
# Backwards compatibility
AVALON_CONTAINER_ID = "pyblish.avalon.container"
AVALON_INSTANCE_ID = "pyblish.avalon.instance"
# TODO get extensions from host implementations
HOST_WORKFILE_EXTENSIONS = {
"blender": [".blend"],
"celaction": [".scn"],
"tvpaint": [".tvpp"],
"fusion": [".comp"],
"harmony": [".zip"],
"houdini": [".hip", ".hiplc", ".hipnc"],
"maya": [".ma", ".mb"],
"nuke": [".nk"],
"hiero": [".hrox"],
"photoshop": [".psd", ".psb"],
"premiere": [".prproj"],
"resolve": [".drp"],
"aftereffects": [".aep"]
}

View file

@ -788,6 +788,11 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
colorspace = product.colorspace
break
if isinstance(files, (list, tuple)):
files = [os.path.basename(f) for f in files]
else:
files = os.path.basename(files)
rep = {
"name": ext,
"ext": ext,

View file

@ -17,8 +17,7 @@ from ayon_core.pipeline.load import get_representation_path_with_anatomy
from ayon_core.pipeline.delivery import (
get_format_dict,
check_destination_path,
deliver_single_file,
deliver_sequence,
deliver_single_file
)
@ -231,51 +230,55 @@ class DeliveryOptionsDialog(QtWidgets.QDialog):
self.log
]
if repre.get("files"):
src_paths = []
for repre_file in repre["files"]:
src_path = self.anatomy.fill_root(repre_file["path"])
src_paths.append(src_path)
sources_and_frames = collect_frames(src_paths)
# TODO: This will currently incorrectly detect 'resources'
# that are published along with the publish, because those should
# not adhere to the template directly but are ingested in a
# customized way. For example, maya look textures or any publish
# that directly adds files into `instance.data["transfers"]`
src_paths = []
for repre_file in repre["files"]:
src_path = self.anatomy.fill_root(repre_file["path"])
src_paths.append(src_path)
sources_and_frames = collect_frames(src_paths)
frames = set(sources_and_frames.values())
frames.discard(None)
first_frame = None
if frames:
first_frame = min(frames)
frames = set(sources_and_frames.values())
frames.discard(None)
first_frame = None
if frames:
first_frame = min(frames)
for src_path, frame in sources_and_frames.items():
args[0] = src_path
# Renumber frames
if renumber_frame and frame is not None:
# Calculate offset between
# first frame and current frame
# - '0' for first frame
offset = frame_offset - int(first_frame)
# Add offset to new frame start
dst_frame = int(frame) + offset
if dst_frame < 0:
msg = "Renumber frame has a smaller number than original frame" # noqa
report_items[msg].append(src_path)
self.log.warning("{} <{}>".format(
msg, dst_frame))
continue
frame = dst_frame
for src_path, frame in sources_and_frames.items():
args[0] = src_path
# Renumber frames
if renumber_frame and frame is not None:
# Calculate offset between
# first frame and current frame
# - '0' for first frame
offset = frame_offset - int(first_frame)
# Add offset to new frame start
dst_frame = int(frame) + offset
if dst_frame < 0:
msg = "Renumber frame has a smaller number than original frame" # noqa
report_items[msg].append(src_path)
self.log.warning("{} <{}>".format(
msg, dst_frame))
continue
frame = dst_frame
if frame is not None:
if frame is not None:
if repre["context"].get("frame"):
anatomy_data["frame"] = frame
new_report_items, uploaded = deliver_single_file(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)
else: # fallback for Pype2 and representations without files
frame = repre["context"].get("frame")
if frame:
repre["context"]["frame"] = len(str(frame)) * "#"
if not frame:
new_report_items, uploaded = deliver_single_file(*args)
else:
new_report_items, uploaded = deliver_sequence(*args)
elif repre["context"].get("udim"):
anatomy_data["udim"] = frame
else:
# Fallback
self.log.warning(
"Representation context has no frame or udim"
" data. Supplying sequence frame to '{frame}'"
" formatting data."
)
anatomy_data["frame"] = frame
new_report_items, uploaded = deliver_single_file(*args)
report_items.update(new_report_items)
self._update_progress(uploaded)

View file

@ -122,13 +122,22 @@ class ExtractOIIOTranscode(publish.Extractor):
transcoding_type = output_def["transcoding_type"]
target_colorspace = view = display = None
# NOTE: we use colorspace_data as the fallback values for
# the target colorspace.
if transcoding_type == "colorspace":
# TODO: Should we fallback to the colorspace
# (which used as source above) ?
# or should we compute the target colorspace from
# current view and display ?
target_colorspace = (output_def["colorspace"] or
colorspace_data.get("colorspace"))
else:
view = output_def["view"] or colorspace_data.get("view")
display = (output_def["display"] or
colorspace_data.get("display"))
elif transcoding_type == "display_view":
display_view = output_def["display_view"]
view = display_view["view"] or colorspace_data.get("view")
display = (
display_view["display"]
or colorspace_data.get("display")
)
# both could be already collected by DCC,
# but could be overwritten when transcoding
@ -192,7 +201,7 @@ class ExtractOIIOTranscode(publish.Extractor):
new_repre["files"] = new_repre["files"][0]
# If the source representation has "review" tag, but its not
# part of the output defintion tags, then both the
# part of the output definition tags, then both the
# representations will be transcoded in ExtractReview and
# their outputs will clash in integration.
if "review" in repre.get("tags", []):

View file

@ -509,8 +509,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
if not is_sequence_representation:
files = [files]
if any(os.path.isabs(fname) for fname in files):
raise KnownPublishError("Given file names contain full paths")
for fname in files:
if os.path.isabs(fname):
raise KnownPublishError(
f"Representation file names contains full paths: {fname}"
)
if not is_sequence_representation:
return

View file

@ -9,7 +9,14 @@ from ayon_api import (
class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
"""Connecting version level dependency links"""
"""Connecting version level dependency links
Handles links:
- generative - what gets produced from workfile
- reference - what was loaded into workfile
It expects workfile instance is being published.
"""
order = pyblish.api.IntegratorOrder + 0.2
label = "Connect Dependency InputLinks AYON"
@ -47,6 +54,11 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
self.create_links_on_server(context, new_links_by_type)
def split_instances(self, context):
"""Separates published instances into workfile and other
Returns:
(tuple(pyblish.plugin.Instance), list(pyblish.plugin.Instance))
"""
workfile_instance = None
other_instances = []
@ -83,6 +95,15 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
def create_workfile_links(
self, workfile_instance, other_instances, new_links_by_type
):
"""Adds links (generative and reference) for workfile.
Args:
workfile_instance (pyblish.plugin.Instance): published workfile
other_instances (list[pyblish.plugin.Instance]): other published
instances
new_links_by_type (dict[str, list[str]]): dictionary collecting new
created links by its type
"""
if workfile_instance is None:
self.log.warn("No workfile in this publish session.")
return
@ -97,7 +118,7 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin):
instance.data["versionEntity"]["id"],
)
loaded_versions = workfile_instance.context.get("loadedVersions")
loaded_versions = workfile_instance.context.data.get("loadedVersions")
if not loaded_versions:
return

View file

@ -36,7 +36,8 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin):
label = "Validate File Saved"
order = pyblish.api.ValidatorOrder - 0.1
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter"]
hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter",
"cinema4d"]
actions = [SaveByVersionUpAction, ShowWorkfilesAction]
def process(self, context):

View file

@ -1,6 +1,9 @@
import collections
from ayon_api import get_representations, get_versions_links
from ayon_api import (
get_representations,
get_versions_links,
)
from ayon_core.lib import Logger, NestedCacheItem
from ayon_core.addon import AddonsManager
@ -509,18 +512,19 @@ class SiteSyncModel:
"reference"
)
for link_repre_id in links:
try:
if not self._sitesync_addon.is_representation_on_site(
project_name,
link_repre_id,
site_name
):
print("Adding {} to linked representation: {}".format(
site_name, link_repre_id))
self._sitesync_addon.add_site(
project_name,
link_repre_id,
site_name,
force=False
force=True
)
except Exception:
# do not add/reset working site for references
log.debug("Site present", exc_info=True)
def _get_linked_representation_id(
self,
@ -575,7 +579,7 @@ class SiteSyncModel:
project_name,
versions_to_check,
link_types=link_types,
link_direction="out")
link_direction="in") # looking for 'in'puts for version
versions_to_check = set()
for links in versions_links.values():
@ -584,9 +588,6 @@ class SiteSyncModel:
if link["entityType"] != "version":
continue
entity_id = link["entityId"]
# Skip already found linked version ids
if entity_id in linked_version_ids:
continue
linked_version_ids.add(entity_id)
versions_to_check.add(entity_id)

View file

@ -439,10 +439,13 @@ class PublisherWindow(QtWidgets.QDialog):
def make_sure_is_visible(self):
if self._window_is_visible:
self.setWindowState(QtCore.Qt.WindowActive)
else:
self.show()
self.raise_()
self.activateWindow()
self.showNormal()
def showEvent(self, event):
self._window_is_visible = True
super().showEvent(event)

View file

@ -4,6 +4,29 @@ from typing import Any
from .publish_plugins import DEFAULT_PUBLISH_VALUES
def _convert_imageio_configs_0_4_5(overrides):
"""Imageio config settings did change to profiles since 0.4.5."""
imageio_overrides = overrides.get("imageio") or {}
# make sure settings are already converted to profiles
ocio_config_profiles = imageio_overrides.get("ocio_config_profiles")
if not ocio_config_profiles:
return
for profile in ocio_config_profiles:
if profile.get("type") != "product_name":
continue
profile["type"] = "published_product"
profile["published_product"] = {
"product_name": profile.pop("product_name"),
"fallback": {
"type": "builtin_path",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
},
}
def _convert_imageio_configs_0_3_1(overrides):
"""Imageio config settings did change to profiles since 0.3.1. ."""
imageio_overrides = overrides.get("imageio") or {}
@ -71,10 +94,43 @@ def _convert_validate_version_0_3_3(publish_overrides):
validate_version["plugin_state_profiles"] = [profile]
def _conver_publish_plugins(overrides):
def _convert_oiio_transcode_0_4_5(publish_overrides):
"""ExtractOIIOTranscode plugin changed in 0.4.5."""
if "ExtractOIIOTranscode" not in publish_overrides:
return
transcode_profiles = publish_overrides["ExtractOIIOTranscode"].get(
"profiles")
if not transcode_profiles:
return
for profile in transcode_profiles:
outputs = profile.get("outputs")
if outputs is None:
return
for output in outputs:
# Already new settings
if "display_view" in output:
break
# Fix 'display' -> 'display_view' in 'transcoding_type'
transcode_type = output.get("transcoding_type")
if transcode_type == "display":
output["transcoding_type"] = "display_view"
# Convert 'display' and 'view' to new values
output["display_view"] = {
"display": output.pop("display", ""),
"view": output.pop("view", ""),
}
def _convert_publish_plugins(overrides):
if "publish" not in overrides:
return
_convert_validate_version_0_3_3(overrides["publish"])
_convert_oiio_transcode_0_4_5(overrides["publish"])
def convert_settings_overrides(
@ -82,5 +138,6 @@ def convert_settings_overrides(
overrides: dict[str, Any],
) -> dict[str, Any]:
_convert_imageio_configs_0_3_1(overrides)
_conver_publish_plugins(overrides)
_convert_imageio_configs_0_4_5(overrides)
_convert_publish_plugins(overrides)
return overrides

View file

@ -58,7 +58,14 @@ def _ocio_config_profile_types():
return [
{"value": "builtin_path", "label": "AYON built-in OCIO config"},
{"value": "custom_path", "label": "Path to OCIO config"},
{"value": "product_name", "label": "Published product"},
{"value": "published_product", "label": "Published product"},
]
def _fallback_ocio_config_profile_types():
return [
{"value": "builtin_path", "label": "AYON built-in OCIO config"},
{"value": "custom_path", "label": "Path to OCIO config"},
]
@ -76,6 +83,49 @@ def _ocio_built_in_paths():
]
class FallbackProductModel(BaseSettingsModel):
_layout = "expanded"
fallback_type: str = SettingsField(
title="Fallback config type",
enum_resolver=_fallback_ocio_config_profile_types,
conditionalEnum=True,
default="builtin_path",
description=(
"Type of config which needs to be used in case published "
"product is not found."
),
)
builtin_path: str = SettingsField(
"ACES 1.2",
title="Built-in OCIO config",
enum_resolver=_ocio_built_in_paths,
description=(
"AYON ocio addon distributed OCIO config. "
"Activated addon in bundle is required: 'ayon_ocio' >= 1.1.1"
),
)
custom_path: str = SettingsField(
"",
title="OCIO config path",
description="Path to OCIO config. Anatomy formatting is supported.",
)
class PublishedProductModel(BaseSettingsModel):
_layout = "expanded"
product_name: str = SettingsField(
"",
title="Product name",
description=(
"Context related published product name to get OCIO config from. "
"Partial match is supported via use of regex expression."
),
)
fallback: FallbackProductModel = SettingsField(
default_factory=FallbackProductModel,
)
class CoreImageIOConfigProfilesModel(BaseSettingsModel):
_layout = "expanded"
host_names: list[str] = SettingsField(
@ -102,19 +152,19 @@ class CoreImageIOConfigProfilesModel(BaseSettingsModel):
"ACES 1.2",
title="Built-in OCIO config",
enum_resolver=_ocio_built_in_paths,
description=(
"AYON ocio addon distributed OCIO config. "
"Activated addon in bundle is required: 'ayon_ocio' >= 1.1.1"
),
)
custom_path: str = SettingsField(
"",
title="OCIO config path",
description="Path to OCIO config. Anatomy formatting is supported.",
)
product_name: str = SettingsField(
"",
title="Product name",
description=(
"Published product name to get OCIO config from. "
"Partial match is supported."
),
published_product: PublishedProductModel = SettingsField(
default_factory=PublishedProductModel,
title="Published product",
)
@ -294,7 +344,14 @@ DEFAULT_VALUES = {
"type": "builtin_path",
"builtin_path": "{BUILTIN_OCIO_ROOT}/aces_1.2/config.ocio",
"custom_path": "",
"product_name": "",
"published_product": {
"product_name": "",
"fallback": {
"fallback_type": "builtin_path",
"builtin_path": "ACES 1.2",
"custom_path": ""
}
}
}
],
"file_rules": {

View file

@ -268,13 +268,36 @@ class ExtractThumbnailModel(BaseSettingsModel):
def _extract_oiio_transcoding_type():
return [
{"value": "colorspace", "label": "Use Colorspace"},
{"value": "display", "label": "Use Display&View"}
{"value": "display_view", "label": "Use Display&View"}
]
class OIIOToolArgumentsModel(BaseSettingsModel):
additional_command_args: list[str] = SettingsField(
default_factory=list, title="Arguments")
default_factory=list,
title="Arguments",
description="Additional command line arguments for *oiiotool*."
)
class UseDisplayViewModel(BaseSettingsModel):
_layout = "expanded"
display: str = SettingsField(
"",
title="Target Display",
description=(
"Display of the target transform. If left empty, the"
" source Display value will be used."
)
)
view: str = SettingsField(
"",
title="Target View",
description=(
"View of the target transform. If left empty, the"
" source View value will be used."
)
)
class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
@ -285,22 +308,57 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel):
description="Output name (no space)",
regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$",
)
extension: str = SettingsField("", title="Extension")
extension: str = SettingsField(
"",
title="Extension",
description=(
"Target extension. If left empty, original"
" extension is used."
),
)
transcoding_type: str = SettingsField(
"colorspace",
title="Transcoding type",
enum_resolver=_extract_oiio_transcoding_type
enum_resolver=_extract_oiio_transcoding_type,
conditionalEnum=True,
description=(
"Select the transcoding type for your output, choosing either "
"*Colorspace* or *Display&View* transform."
" Only one option can be applied per output definition."
),
)
colorspace: str = SettingsField("", title="Colorspace")
display: str = SettingsField("", title="Display")
view: str = SettingsField("", title="View")
colorspace: str = SettingsField(
"",
title="Target Colorspace",
description=(
"Choose the desired target colorspace, confirming its availability"
" in the active OCIO config. If left empty, the"
" source colorspace value will be used, resulting in no"
" colorspace conversion."
)
)
display_view: UseDisplayViewModel = SettingsField(
title="Use Display&View",
default_factory=UseDisplayViewModel
)
oiiotool_args: OIIOToolArgumentsModel = SettingsField(
default_factory=OIIOToolArgumentsModel,
title="OIIOtool arguments")
tags: list[str] = SettingsField(default_factory=list, title="Tags")
tags: list[str] = SettingsField(
default_factory=list,
title="Tags",
description=(
"Additional tags that will be added to the created representation."
"\nAdd *review* tag to create review from the transcoded"
" representation instead of the original."
)
)
custom_tags: list[str] = SettingsField(
default_factory=list, title="Custom Tags"
default_factory=list,
title="Custom Tags",
description="Additional custom tags that will be added to the created representation."
)
@ -328,7 +386,13 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel):
)
delete_original: bool = SettingsField(
True,
title="Delete Original Representation"
title="Delete Original Representation",
description=(
"Choose to preserve or remove the original representation.\n"
"Keep in mind that if the transcoded representation includes"
" a `review` tag, it will take precedence over"
" the original for creating reviews."
),
)
outputs: list[ExtractOIIOTranscodeOutputModel] = SettingsField(
default_factory=list,
@ -371,7 +435,7 @@ class ExtractReviewFFmpegModel(BaseSettingsModel):
def extract_review_filter_enum():
return [
{
"value": "everytime",
"value": "everytime", # codespell:ignore everytime
"label": "Always"
},
{
@ -393,7 +457,7 @@ class ExtractReviewFilterModel(BaseSettingsModel):
default_factory=list, title="Custom Tags"
)
single_frame_filter: str = SettingsField(
"everytime",
"everytime", # codespell:ignore everytime
description=(
"Use output <b>always</b> / only if input <b>is 1 frame</b>"
" image / only if has <b>2+ frames</b> or <b>is video</b>"
@ -791,7 +855,7 @@ class IntegrateHeroVersionModel(BaseSettingsModel):
class CleanUpModel(BaseSettingsModel):
_isGroup = True
paterns: list[str] = SettingsField(
paterns: list[str] = SettingsField( # codespell:ignore paterns
default_factory=list,
title="Patterns (regex)"
)
@ -1225,7 +1289,7 @@ DEFAULT_PUBLISH_VALUES = {
"use_hardlinks": False
},
"CleanUp": {
"paterns": [],
"paterns": [], # codespell:ignore paterns
"remove_temp_renders": False
},
"CleanUpFarm": {