-
Notifications
You must be signed in to change notification settings - Fork 34
/
workfile_template_builder.py
1930 lines (1519 loc) · 61.9 KB
/
workfile_template_builder.py
1
2
3
4
5
6
7
8
9
10
11
12
13
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
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Workfile build mechanism using workfile templates.
Build templates are manually prepared using plugin definitions which create
placeholders inside the template which are populated on import.
This approach is very explicit to achive very specific build logic that can be
targeted by task types and names.
Placeholders are created using placeholder plugins which should care about
logic and data of placeholder items. 'PlaceholderItem' is used to keep track
about it's progress.
"""
import os
import re
import collections
import copy
from abc import ABCMeta, abstractmethod
import six
from ayon_api import (
get_folders,
get_folder_by_path,
get_folder_links,
get_task_by_name,
get_products,
get_last_versions,
get_representations,
)
from ayon_core.settings import get_project_settings
from ayon_core.host import IWorkfileHost, HostBase
from ayon_core.lib import (
Logger,
StringTemplate,
filter_profiles,
attribute_definitions,
)
from ayon_core.lib.attribute_definitions import get_attributes_keys
from ayon_core.pipeline import Anatomy
from ayon_core.pipeline.load import (
get_loaders_by_name,
get_representation_contexts,
load_with_repre_context,
)
from ayon_core.pipeline.create import (
discover_legacy_creator_plugins,
CreateContext,
)
_NOT_SET = object()
class TemplateNotFound(Exception):
"""Exception raised when template does not exist."""
pass
class TemplateProfileNotFound(Exception):
"""Exception raised when current profile
doesn't match any template profile"""
pass
class TemplateAlreadyImported(Exception):
"""Error raised when Template was already imported by host for
this session"""
pass
class TemplateLoadFailed(Exception):
"""Error raised whend Template loader was unable to load the template"""
pass
@six.add_metaclass(ABCMeta)
class AbstractTemplateBuilder(object):
"""Abstraction of Template Builder.
Builder cares about context, shared data, cache, discovery of plugins
and trigger logic. Provides public api for host workfile build systen.
Rest of logic is based on plugins that care about collection and creation
of placeholder items.
Population of placeholders happens in loops. Each loop will collect all
available placeholders, skip already populated, and populate the rest.
Builder item has 2 types of shared data. Refresh lifetime which are cleared
on refresh and populate lifetime which are cleared after loop of
placeholder population.
Args:
host (Union[HostBase, ModuleType]): Implementation of host.
"""
_log = None
use_legacy_creators = False
def __init__(self, host):
# Get host name
if isinstance(host, HostBase):
host_name = host.name
else:
host_name = os.environ.get("AYON_HOST_NAME")
self._host = host
self._host_name = host_name
# Shared data across placeholder plugins
self._shared_data = {}
self._shared_populate_data = {}
# Where created objects of placeholder plugins will be stored
self._placeholder_plugins = None
self._loaders_by_name = None
self._creators_by_name = None
self._create_context = None
self._project_settings = None
self._current_folder_entity = _NOT_SET
self._current_task_entity = _NOT_SET
self._linked_folder_entities = _NOT_SET
@property
def project_name(self):
if isinstance(self._host, HostBase):
return self._host.get_current_project_name()
return os.getenv("AYON_PROJECT_NAME")
@property
def current_folder_path(self):
if isinstance(self._host, HostBase):
return self._host.get_current_folder_path()
return os.getenv("AYON_FOLDER_PATH")
@property
def current_task_name(self):
if isinstance(self._host, HostBase):
return self._host.get_current_task_name()
return os.getenv("AYON_TASK_NAME")
def get_current_context(self):
if isinstance(self._host, HostBase):
return self._host.get_current_context()
return {
"project_name": self.project_name,
"folder_path": self.current_folder_path,
"task_name": self.current_task_name
}
@property
def project_settings(self):
if self._project_settings is None:
self._project_settings = get_project_settings(self.project_name)
return self._project_settings
@property
def current_folder_entity(self):
if self._current_folder_entity is _NOT_SET:
self._current_folder_entity = get_folder_by_path(
self.project_name, self.current_folder_path
)
return self._current_folder_entity
@property
def linked_folder_entities(self):
if self._linked_folder_entities is _NOT_SET:
self._linked_folder_entities = self._get_linked_folder_entities()
return self._linked_folder_entities
@property
def current_task_entity(self):
if self._current_task_entity is _NOT_SET:
task_entity = None
folder_entity = self.current_folder_entity
if folder_entity:
task_entity = get_task_by_name(
self.project_name,
folder_entity["id"],
self.current_task_name
)
self._current_task_entity = task_entity
return self._current_task_entity
@property
def current_task_type(self):
task_entity = self.current_task_entity
if task_entity:
return task_entity["taskType"]
return None
@property
def create_context(self):
if self._create_context is None:
self._create_context = CreateContext(
self.host,
discover_publish_plugins=False,
headless=True
)
return self._create_context
def get_placeholder_plugin_classes(self):
"""Get placeholder plugin classes that can be used to build template.
Default implementation looks for method
'get_workfile_build_placeholder_plugins' on host.
Returns:
List[PlaceholderPlugin]: Plugin classes available for host.
"""
if hasattr(self._host, "get_workfile_build_placeholder_plugins"):
return self._host.get_workfile_build_placeholder_plugins()
return []
@property
def host(self):
"""Access to host implementation.
Returns:
Union[HostBase, ModuleType]: Implementation of host.
"""
return self._host
@property
def host_name(self):
"""Name of 'host' implementation.
Returns:
str: Host's name.
"""
return self._host_name
@property
def log(self):
"""Dynamically created logger for the plugin."""
if self._log is None:
self._log = Logger.get_logger(repr(self))
return self._log
def refresh(self):
"""Reset cached data."""
self._placeholder_plugins = None
self._loaders_by_name = None
self._creators_by_name = None
self._current_folder_entity = _NOT_SET
self._current_task_entity = _NOT_SET
self._linked_folder_entities = _NOT_SET
self._project_settings = None
self.clear_shared_data()
self.clear_shared_populate_data()
def get_loaders_by_name(self):
if self._loaders_by_name is None:
self._loaders_by_name = get_loaders_by_name()
return self._loaders_by_name
def _get_linked_folder_entities(self):
project_name = self.project_name
folder_entity = self.current_folder_entity
if not folder_entity:
return []
links = get_folder_links(
project_name, folder_entity["id"], link_direction="in"
)
linked_folder_ids = {
link["entityId"]
for link in links
if link["entityType"] == "folder"
}
return list(get_folders(project_name, folder_ids=linked_folder_ids))
def _collect_legacy_creators(self):
creators_by_name = {}
for creator in discover_legacy_creator_plugins():
if not creator.enabled:
continue
creator_name = creator.__name__
if creator_name in creators_by_name:
raise KeyError(
"Duplicated creator name {} !".format(creator_name)
)
creators_by_name[creator_name] = creator
self._creators_by_name = creators_by_name
def _collect_creators(self):
self._creators_by_name = dict(self.create_context.creators)
def get_creators_by_name(self):
if self._creators_by_name is None:
if self.use_legacy_creators:
self._collect_legacy_creators()
else:
self._collect_creators()
return self._creators_by_name
def get_shared_data(self, key):
"""Receive shared data across plugins and placeholders.
This can be used to scroll scene only once to look for placeholder
items if the storing is unified but each placeholder plugin would have
to call it again.
Args:
key (str): Key under which are shared data stored.
Returns:
Union[None, Any]: None if key was not set.
"""
return self._shared_data.get(key)
def set_shared_data(self, key, value):
"""Store share data across plugins and placeholders.
Store data that can be afterwards accessed from any future call. It
is good practice to check if the same value is not already stored under
different key or if the key is not already used for something else.
Key should be self explanatory to content.
- wrong: 'folder'
- good: 'folder_name'
Args:
key (str): Key under which is key stored.
value (Any): Value that should be stored under the key.
"""
self._shared_data[key] = value
def clear_shared_data(self):
"""Clear shared data.
Method only clear shared data to default state.
"""
self._shared_data = {}
def clear_shared_populate_data(self):
"""Receive shared data across plugins and placeholders.
These data are cleared after each loop of populating of template.
This can be used to scroll scene only once to look for placeholder
items if the storing is unified but each placeholder plugin would have
to call it again.
Args:
key (str): Key under which are shared data stored.
Returns:
Union[None, Any]: None if key was not set.
"""
self._shared_populate_data = {}
def get_shared_populate_data(self, key):
"""Store share populate data across plugins and placeholders.
These data are cleared after each loop of populating of template.
Store data that can be afterwards accessed from any future call. It
is good practice to check if the same value is not already stored under
different key or if the key is not already used for something else.
Key should be self explanatory to content.
- wrong: 'folder'
- good: 'folder_path'
Args:
key (str): Key under which is key stored.
value (Any): Value that should be stored under the key.
"""
return self._shared_populate_data.get(key)
def set_shared_populate_data(self, key, value):
"""Store share populate data across plugins and placeholders.
These data are cleared after each loop of populating of template.
Store data that can be afterwards accessed from any future call. It
is good practice to check if the same value is not already stored under
different key or if the key is not already used for something else.
Key should be self explanatory to content.
- wrong: 'folder'
- good: 'folder_path'
Args:
key (str): Key under which is key stored.
value (Any): Value that should be stored under the key.
"""
self._shared_populate_data[key] = value
@property
def placeholder_plugins(self):
"""Access to initialized placeholder plugins.
Returns:
List[PlaceholderPlugin]: Initialized plugins available for host.
"""
if self._placeholder_plugins is None:
placeholder_plugins = {}
for cls in self.get_placeholder_plugin_classes():
try:
plugin = cls(self)
placeholder_plugins[plugin.identifier] = plugin
except Exception:
self.log.warning(
"Failed to initialize placeholder plugin {}".format(
cls.__name__
),
exc_info=True
)
self._placeholder_plugins = placeholder_plugins
return self._placeholder_plugins
def create_placeholder(self, plugin_identifier, placeholder_data):
"""Create new placeholder using plugin identifier and data.
Args:
plugin_identifier (str): Identifier of plugin. That's how builder
know which plugin should be used.
placeholder_data (Dict[str, Any]): Placeholder item data. They
should match options required by the plugin.
Returns:
PlaceholderItem: Created placeholder item.
"""
plugin = self.placeholder_plugins[plugin_identifier]
return plugin.create_placeholder(placeholder_data)
def get_placeholders(self):
"""Collect placeholder items from scene.
Each placeholder plugin can collect it's placeholders and return them.
This method does not use cached values but always go through the scene.
Returns:
List[PlaceholderItem]: Sorted placeholder items.
"""
placeholders = []
for placeholder_plugin in self.placeholder_plugins.values():
result = placeholder_plugin.collect_placeholders()
if result:
placeholders.extend(result)
return list(sorted(
placeholders,
key=lambda i: i.order
))
def build_template(
self,
template_path=None,
level_limit=None,
keep_placeholders=None,
create_first_version=None,
workfile_creation_enabled=False
):
"""Main callback for building workfile from template path.
Todo:
Handle report of populated placeholders from
'populate_scene_placeholders' to be shown to a user.
Args:
template_path (str): Path to a template file with placeholders.
Template from settings 'get_template_preset' used when not
passed.
level_limit (int): Limit of populate loops. Related to
'populate_scene_placeholders' method.
keep_placeholders (bool): Add flag to placeholder data for
hosts to decide if they want to remove
placeholder after it is used.
create_first_version (bool): create first version of a workfile
workfile_creation_enabled (bool): If True, it might create
first version but ignore
process if version is created
"""
if any(
value is None
for value in [
template_path,
keep_placeholders,
create_first_version,
]
):
template_preset = self.get_template_preset()
if template_path is None:
template_path = template_preset["path"]
if keep_placeholders is None:
keep_placeholders = template_preset["keep_placeholder"]
if create_first_version is None:
create_first_version = template_preset["create_first_version"]
# check if first version is created
created_version_workfile = False
if create_first_version:
created_version_workfile = self.create_first_workfile_version()
# if first version is created, import template
# and populate placeholders
if (
create_first_version
and workfile_creation_enabled
and created_version_workfile
):
self.import_template(template_path)
self.populate_scene_placeholders(
level_limit, keep_placeholders)
# save workfile after template is populated
self.save_workfile(created_version_workfile)
# ignore process if first workfile is enabled
# but a version is already created
if workfile_creation_enabled:
return
self.import_template(template_path)
self.populate_scene_placeholders(
level_limit, keep_placeholders)
def rebuild_template(self):
"""Go through existing placeholders in scene and update them.
This could not make sense for all plugin types so this is optional
logic for plugins.
Note:
Logic is not importing the template again but using placeholders
that were already available. We should maybe change the method
name.
Question:
Should this also handle subloops as it is possible that another
template is loaded during processing?
"""
if not self.placeholder_plugins:
self.log.info("There are no placeholder plugins available.")
return
placeholders = self.get_placeholders()
if not placeholders:
self.log.info("No placeholders were found.")
return
for placeholder in placeholders:
plugin = placeholder.plugin
plugin.repopulate_placeholder(placeholder)
self.clear_shared_populate_data()
def open_template(self):
"""Open template file with registered host."""
template_preset = self.get_template_preset()
template_path = template_preset["path"]
self.host.open_file(template_path)
@abstractmethod
def import_template(self, template_path):
"""
Import template in current host.
Should load the content of template into scene so
'populate_scene_placeholders' can be started.
Args:
template_path (str): Fullpath for current task and
host's template file.
"""
pass
def create_first_workfile_version(self):
"""
Create first version of workfile.
Should load the content of template into scene so
'populate_scene_placeholders' can be started.
Args:
template_path (str): Fullpath for current task and
host's template file.
"""
last_workfile_path = os.environ.get("AYON_LAST_WORKFILE")
self.log.info("__ last_workfile_path: {}".format(last_workfile_path))
if os.path.exists(last_workfile_path):
# ignore in case workfile existence
self.log.info("Workfile already exists, skipping creation.")
return False
# Create first version
self.log.info("Creating first version of workfile.")
self.save_workfile(last_workfile_path)
# Confirm creation of first version
return last_workfile_path
def save_workfile(self, workfile_path):
"""Save workfile in current host."""
# Save current scene, continue to open file
if isinstance(self.host, IWorkfileHost):
self.host.save_workfile(workfile_path)
else:
self.host.save_file(workfile_path)
def _prepare_placeholders(self, placeholders):
"""Run preparation part for placeholders on plugins.
Args:
placeholders (List[PlaceholderItem]): Placeholder items that will
be processed.
"""
# Prepare placeholder items by plugin
plugins_by_identifier = {}
placeholders_by_plugin_id = collections.defaultdict(list)
for placeholder in placeholders:
plugin = placeholder.plugin
identifier = plugin.identifier
plugins_by_identifier[identifier] = plugin
placeholders_by_plugin_id[identifier].append(placeholder)
# Plugin should prepare data for passed placeholders
for identifier, placeholders in placeholders_by_plugin_id.items():
plugin = plugins_by_identifier[identifier]
plugin.prepare_placeholders(placeholders)
def populate_scene_placeholders(
self, level_limit=None, keep_placeholders=None
):
"""Find placeholders in scene using plugins and process them.
This should happen after 'import_template'.
Collect available placeholders from scene. All of them are processed
after that shared data are cleared. Placeholder items are collected
again and if there are any new the loop happens again. This is possible
to change with defying 'level_limit'.
Placeholders are marked as processed so they're not re-processed. To
identify which placeholders were already processed is used
placeholder's 'scene_identifier'.
Args:
level_limit (int): Level of loops that can happen. Default is 1000.
keep_placeholders (bool): Add flag to placeholder data for
hosts to decide if they want to remove
placeholder after it is used.
"""
if not self.placeholder_plugins:
self.log.warning("There are no placeholder plugins available.")
return
placeholders = self.get_placeholders()
if not placeholders:
self.log.warning("No placeholders were found.")
return
# Avoid infinite loop
# - 1000 iterations of placeholders processing must be enough
if not level_limit:
level_limit = 1000
placeholder_by_scene_id = {
placeholder.scene_identifier: placeholder
for placeholder in placeholders
}
all_processed = len(placeholders) == 0
# Counter is checked at the ned of a loop so the loop happens at least
# once.
iter_counter = 0
while not all_processed:
filtered_placeholders = []
for placeholder in placeholders:
if placeholder.finished:
continue
if placeholder.in_progress:
self.log.warning((
"Placeholder that should be processed"
" is already in progress."
))
continue
# add flag for keeping placeholders in scene
# after they are processed
placeholder.data["keep_placeholder"] = keep_placeholders
filtered_placeholders.append(placeholder)
self._prepare_placeholders(filtered_placeholders)
for placeholder in filtered_placeholders:
placeholder.set_in_progress()
placeholder_plugin = placeholder.plugin
try:
placeholder_plugin.populate_placeholder(placeholder)
except Exception as exc:
self.log.warning(
(
"Failed to process placeholder {} with plugin {}"
).format(
placeholder.scene_identifier,
placeholder_plugin.__class__.__name__
),
exc_info=True
)
placeholder.set_failed(exc)
placeholder.set_finished()
# Clear shared data before getting new placeholders
self.clear_shared_populate_data()
iter_counter += 1
if iter_counter >= level_limit:
break
all_processed = True
collected_placeholders = self.get_placeholders()
for placeholder in collected_placeholders:
identifier = placeholder.scene_identifier
if identifier in placeholder_by_scene_id:
continue
all_processed = False
placeholder_by_scene_id[identifier] = placeholder
placeholders.append(placeholder)
self.refresh()
def _get_build_profiles(self):
"""Get build profiles for workfile build template path.
Returns:
List[Dict[str, Any]]: Profiles for template path resolving.
"""
return (
self.project_settings
[self.host_name]
["templated_workfile_build"]
["profiles"]
)
def get_template_preset(self):
"""Unified way how template preset is received usign settings.
Method is dependent on '_get_build_profiles' which should return filter
profiles to resolve path to a template. Default implementation looks
into host settings:
- 'project_settings/{host name}/templated_workfile_build/profiles'
Returns:
dict: Dictionary with `path`, `keep_placeholder` and
`create_first_version` settings from the template preset
for current context.
Raises:
TemplateProfileNotFound: When profiles are not filled.
TemplateLoadFailed: Profile was found but path is not set.
TemplateNotFound: Path was set but file does not exist.
"""
host_name = self.host_name
project_name = self.project_name
task_name = self.current_task_name
task_type = self.current_task_type
build_profiles = self._get_build_profiles()
profile = filter_profiles(
build_profiles,
{
"task_types": task_type,
"task_names": task_name
}
)
if not profile:
raise TemplateProfileNotFound((
"No matching profile found for task '{}' of type '{}' "
"with host '{}'"
).format(task_name, task_type, host_name))
path = profile["path"]
# switch to remove placeholders after they are used
keep_placeholder = profile.get("keep_placeholder")
create_first_version = profile.get("create_first_version")
# backward compatibility, since default is True
if keep_placeholder is None:
keep_placeholder = True
if not path:
raise TemplateLoadFailed((
"Template path is not set.\n"
"Path need to be set in {}\\Template Workfile Build "
"Settings\\Profiles"
).format(host_name.title()))
# Try fill path with environments and anatomy roots
anatomy = Anatomy(project_name)
fill_data = {
key: value
for key, value in os.environ.items()
}
fill_data["root"] = anatomy.roots
fill_data["project"] = {
"name": project_name,
"code": anatomy.project_code,
}
result = StringTemplate.format_template(path, fill_data)
if result.solved:
path = result.normalized()
if path and os.path.exists(path):
self.log.info("Found template at: '{}'".format(path))
return {
"path": path,
"keep_placeholder": keep_placeholder,
"create_first_version": create_first_version
}
solved_path = None
while True:
try:
solved_path = anatomy.path_remapper(path)
except KeyError as missing_key:
raise KeyError(
"Could not solve key '{}' in template path '{}'".format(
missing_key, path))
if solved_path is None:
solved_path = path
if solved_path == path:
break
path = solved_path
solved_path = os.path.normpath(solved_path)
if not os.path.exists(solved_path):
raise TemplateNotFound(
"Template found in AYON settings for task '{}' with host "
"'{}' does not exists. (Not found : {})".format(
task_name, host_name, solved_path))
self.log.info("Found template at: '{}'".format(solved_path))
return {
"path": solved_path,
"keep_placeholder": keep_placeholder,
"create_first_version": create_first_version
}
@six.add_metaclass(ABCMeta)
class PlaceholderPlugin(object):
"""Plugin which care about handling of placeholder items logic.
Plugin create and update placeholders in scene and populate them on
template import. Populating means that based on placeholder data happens
a logic in the scene. Most common logic is to load representation using
loaders or to create instances in scene.
"""
label = None
_log = None
def __init__(self, builder):
self._builder = builder
@property
def builder(self):
"""Access to builder which initialized the plugin.
Returns:
AbstractTemplateBuilder: Loader of template build.
"""
return self._builder
@property
def project_name(self):
return self._builder.project_name
@property
def log(self):
"""Dynamically created logger for the plugin."""
if self._log is None:
self._log = Logger.get_logger(repr(self))
return self._log
@property
def identifier(self):
"""Identifier which will be stored to placeholder.
Default implementation uses class name.
Returns:
str: Unique identifier of placeholder plugin.
"""
return self.__class__.__name__
@abstractmethod
def create_placeholder(self, placeholder_data):
"""Create new placeholder in scene and get it's item.
It matters on the plugin implementation if placeholder will use
selection in scene or create new node.
Args:
placeholder_data (Dict[str, Any]): Data that were created
based on attribute definitions from 'get_placeholder_options'.
Returns:
PlaceholderItem: Created placeholder item.
"""
pass
@abstractmethod
def update_placeholder(self, placeholder_item, placeholder_data):
"""Update placeholder item with new data.
New data should be propagated to object of placeholder item itself
and also into the scene.
Reason:
Some placeholder plugins may require some special way how the
updates should be propagated to object.
Args:
placeholder_item (PlaceholderItem): Object of placeholder that
should be updated.
placeholder_data (Dict[str, Any]): Data related to placeholder.
Should match plugin options.
"""
pass
@abstractmethod
def collect_placeholders(self):
"""Collect placeholders from scene.
Returns:
List[PlaceholderItem]: Placeholder objects.
"""
pass
def get_placeholder_options(self, options=None):
"""Placeholder options for data showed.
Returns:
List[AbstractAttrDef]: Attribute definitions of
placeholder options.
"""
return []
def get_placeholder_keys(self):
"""Get placeholder keys that are stored in scene.
Returns:
Set[str]: Key of placeholder keys that are stored in scene.
"""
option_keys = get_attributes_keys(self.get_placeholder_options())
option_keys.add("plugin_identifier")
return option_keys