14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192 | class ReplacePlaceholders(PreLaunchHook):
"""Search and replace placeholders for path and replace them from Ayon
It updates copied variant of template workfile if first version of workfile
doesn't exist, it modifies last opened workfile if exists (TODO).
Some read and write nodes might contain text Ayon placeholders describing
what product, version and representation should be loaded.
Expected placeholder format is PLACEHOLDER_VALUE_PATTERN.
Implemented 'placeholder' in 'placeholder':
- asset_name - {currentAsset} or any asset_name
- version - {latest} or {hero} or any integer value
(AYON.{currentAsset}.modelMain.{latest}.abc
AYON.characterB.modelMain.{hero}.abc
AYON.{currentAsset}.modelMaoin.1.abc)
"""
order = 25
app_groups = {"wrap"}
launch_types = {LaunchTypes.local}
PLACEHOLDER_PATTERN = "\"value\": \"^AYON\.\".*$"
def execute(self):
last_workfile_path = self.data.get("last_workfile_path")
if not last_workfile_path or not os.path.exists(last_workfile_path):
self.log.warning((
"Last workfile was not collected."
" No placeholder replacement is possible."
))
return
self._fill_placeholders(last_workfile_path)
self.log.info(f"Updating: \"{last_workfile_path}\"")
def _fill_placeholders(self, workfile_path):
"""Replaces placeholders in existing `workfile_path`.
Args:
workfile_path (str): real workfile in 'work' area that will be
opened, already copied from template
Searches for PLACEHOLDER_PATTERN, tries to fill it with dynamic values
and replaces it.
"""
with open(workfile_path, "r") as f:
content = json.load(f)
orig_metadata = (content.get("metadata", {})
.get("AYON_NODE_METADATA", {}))
containers = []
stored_containers = {item["nodeId"]: item
for item in orig_metadata
if item.get("nodeId") is not None}
for node_name, node in content["nodes"].items():
load_placeholder = self._get_load_placeholder(
node, stored_containers)
if load_placeholder:
try:
containers.append(
self._containerize_load_placeholder(
node,
node_name,
load_placeholder,
workfile_path
)
)
except PlaceholderFillException as exp:
self.log.warning(f"{node_name} replacement "
f"failed with '{str(exp)}'")
if node_name.startswith("AYON_"): #TODO
file_path = node["params"]["fileName"]["value"]
workfile_version = f"v{get_version_from_path(workfile_path)}" # noqa
file_path = self._update_version_placeholder(
workfile_version, file_path)
node["params"]["fileName"]["value"] = file_path
# keep untouched meta
for existing_node_meta in orig_metadata:
if existing_node_meta["id"] != AVALON_CONTAINER_ID:
containers.append(existing_node_meta)
if not containers and not orig_metadata:
return
if not content.get("metadata"):
content["metadata"] = {}
content["metadata"]["AYON_NODE_METADATA"] = containers
backup_path = f"{workfile_path}.bck"
shutil.copy(workfile_path, backup_path)
with open(workfile_path, "w") as fp:
json.dump(content, fp, indent=4)
os.unlink(backup_path)
def _update_version_placeholder(self, workfile_version, file_path):
"""Searches for {version} or 'v000' placeholder in output file path"""
if "{version}" in file_path:
file_path = file_path.replace("{version}",
workfile_version)
else:
version_from_path = get_version_from_path(file_path)
if version_from_path:
file_path = file_path.replace(version_from_path,
workfile_version)
return file_path
def _get_load_placeholder(self, node, stored_containers):
"""Checks if node contains placeholder for loaded items.
It might be directly in file path of the node (for fresh template), or
resolved and saved in `stored_containers`.
Args:
node (dict): dictionary of node from Wrap file
stored_containers (dict): id -> container metadata for resolved
and saved loaded container.
Returns:
(str): placeholder in format `AYON.{currentAsset}.renderMain...`
"""
if not node.get("params"):
return None
file_path_doc = node["params"].get("fileName")
if not file_path_doc:
return None
stored_node_meta = stored_containers.get(node["nodeId"])
return self._get_placeholder(file_path_doc, stored_node_meta)
def _containerize_load_placeholder(self, node, node_name,
placeholder, workfile_path):
"""Resolves string placeholder with actual path to product.
Args:
node (dict): node dictionary from Wrap
node_name (str):
placeholder (str): placeholder in format
`AYON.{currentAsset}.renderMain...`
workfile_path (str): abs path to workfile to store into metadata
Returns:
(dict): AYON container metadata
"""
repre, filled_value = api.fill_placeholder(placeholder,
workfile_path,
self.data)
node["params"]["fileName"]["value"] = filled_value
data = {
"original_value": placeholder,
"nodeId": node["nodeId"],
"node_name": node_name
}
context = self.data
context["representation"] = repre
return api.containerise(
name=os.path.basename(filled_value),
namespace=workfile_path,
loader="FileLoader",
context=context,
data=data
)
def _get_placeholder(self, file_info, stored_node_meta):
"""Gets placeholder from file path of node or stored metadata.
Node filepath has precedence as it could be changed by artist,
metadata might contain obsolete value.
"""
if file_info["value"].startswith("AYON"):
return file_info["value"]
if stored_node_meta and stored_node_meta["id"] == AVALON_CONTAINER_ID:
return stored_node_meta["original_value"]
|