Skip to content

create

AttributeValues

Container which keep values of Attribute definitions.

Goal is to have one object which hold values of attribute definitions for single instance.

Has dictionary like methods. Not all of them are allowed all the time.

Parameters:

Name Type Description Default
parent Union[CreatedInstance, PublishAttributes]

Parent object.

required
key str

Key of attribute values.

required
attr_defs List[AbstractAttrDef]

Definitions of value type and properties.

required
values dict

Values after possible conversion.

required
origin_data dict

Values loaded from host before conversion.

None
Source code in client/ayon_core/pipeline/create/structures.py
 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
class AttributeValues:
    """Container which keep values of Attribute definitions.

    Goal is to have one object which hold values of attribute definitions for
    single instance.

    Has dictionary like methods. Not all of them are allowed all the time.

    Args:
        parent (Union[CreatedInstance, PublishAttributes]): Parent object.
        key (str): Key of attribute values.
        attr_defs (List[AbstractAttrDef]): Definitions of value type
            and properties.
        values (dict): Values after possible conversion.
        origin_data (dict): Values loaded from host before conversion.

    """
    def __init__(self, parent, key, attr_defs, values, origin_data=None):
        self._parent = parent
        self._key = key
        if origin_data is None:
            origin_data = copy.deepcopy(values)
        self._origin_data = origin_data

        attr_defs_by_key = {
            attr_def.key: attr_def
            for attr_def in attr_defs
            if attr_def.is_value_def
        }
        for key, value in values.items():
            if key not in attr_defs_by_key:
                new_def = UnknownDef(key, label=key, default=value)
                attr_defs.append(new_def)
                attr_defs_by_key[key] = new_def

        self._attr_defs = attr_defs
        self._attr_defs_by_key = attr_defs_by_key

        self._data = {}
        for attr_def in attr_defs:
            value = values.get(attr_def.key)
            if value is None:
                continue
            converted_value = attr_def.convert_value(value)
            if converted_value == value:
                self._data[attr_def.key] = value

    def __setitem__(self, key, value):
        if key not in self._attr_defs_by_key:
            raise KeyError("Key \"{}\" was not found.".format(key))

        self.update({key: value})

    def __getitem__(self, key):
        if key not in self._attr_defs_by_key:
            return self._data[key]
        return self._data.get(key, self._attr_defs_by_key[key].default)

    def __contains__(self, key):
        return key in self._attr_defs_by_key

    def __iter__(self):
        for key in self._attr_defs_by_key:
            yield key

    def get(self, key, default=None):
        if key in self._attr_defs_by_key:
            return self[key]
        return default

    def keys(self):
        return self._attr_defs_by_key.keys()

    def values(self):
        for key in self._attr_defs_by_key.keys():
            yield self._data.get(key)

    def items(self):
        for key in self._attr_defs_by_key.keys():
            yield key, self._data.get(key)

    def get_attr_def(self, key, default=None):
        return self._attr_defs_by_key.get(key, default)

    def update(self, value):
        changes = {}
        for _key, _value in dict(value).items():
            if _key in self._data and self._data.get(_key) == _value:
                continue
            self._data[_key] = _value
            changes[_key] = _value

        if changes:
            self._parent.attribute_value_changed(self._key, changes)

    def pop(self, key, default=None):
        has_key = key in self._data
        value = self._data.pop(key, default)
        # Remove attribute definition if is 'UnknownDef'
        # - gives option to get rid of unknown values
        attr_def = self._attr_defs_by_key.get(key)
        if isinstance(attr_def, UnknownDef):
            self._attr_defs_by_key.pop(key)
            self._attr_defs.remove(attr_def)
        elif has_key:
            self._parent.attribute_value_changed(self._key, {key: None})
        return value

    def reset_values(self):
        self._data = {}

    def mark_as_stored(self):
        self._origin_data = copy.deepcopy(self._data)

    @property
    def attr_defs(self):
        """Pointer to attribute definitions.

        Returns:
            List[AbstractAttrDef]: Attribute definitions.
        """

        return list(self._attr_defs)

    @property
    def origin_data(self):
        return copy.deepcopy(self._origin_data)

    def data_to_store(self):
        """Create new dictionary with data to store.

        Returns:
            Dict[str, Any]: Attribute values that should be stored.
        """

        output = {}
        for key in self._data:
            output[key] = self[key]

        for key, attr_def in self._attr_defs_by_key.items():
            if key not in output:
                output[key] = attr_def.default
        return output

    def get_serialized_attr_defs(self):
        """Serialize attribute definitions to json serializable types.

        Returns:
            List[Dict[str, Any]]: Serialized attribute definitions.
        """

        return serialize_attr_defs(self._attr_defs)

attr_defs property

Pointer to attribute definitions.

Returns:

Type Description

List[AbstractAttrDef]: Attribute definitions.

data_to_store()

Create new dictionary with data to store.

Returns:

Type Description

Dict[str, Any]: Attribute values that should be stored.

Source code in client/ayon_core/pipeline/create/structures.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def data_to_store(self):
    """Create new dictionary with data to store.

    Returns:
        Dict[str, Any]: Attribute values that should be stored.
    """

    output = {}
    for key in self._data:
        output[key] = self[key]

    for key, attr_def in self._attr_defs_by_key.items():
        if key not in output:
            output[key] = attr_def.default
    return output

get_serialized_attr_defs()

Serialize attribute definitions to json serializable types.

Returns:

Type Description

List[Dict[str, Any]]: Serialized attribute definitions.

Source code in client/ayon_core/pipeline/create/structures.py
222
223
224
225
226
227
228
229
def get_serialized_attr_defs(self):
    """Serialize attribute definitions to json serializable types.

    Returns:
        List[Dict[str, Any]]: Serialized attribute definitions.
    """

    return serialize_attr_defs(self._attr_defs)

AutoCreator

Bases: BaseCreator

Creator which is automatically triggered without user interaction.

Can be used e.g. for workfile.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
959
960
961
962
963
964
965
966
967
class AutoCreator(BaseCreator):
    """Creator which is automatically triggered without user interaction.

    Can be used e.g. for `workfile`.
    """

    def remove_instances(self, instances):
        """Skip removal."""
        pass

remove_instances(instances)

Skip removal.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
965
966
967
def remove_instances(self, instances):
    """Skip removal."""
    pass

BaseCreator

Bases: ABC

Plugin that create and modify instance data before publishing process.

We should maybe find better name as creation is only one part of its logic and to avoid expectations that it is the same as avalon.api.Creator.

Single object should be used for multiple instances instead of single instance per one creator object. Do not store temp data or mid-process data to self if it's not Plugin specific.

Parameters:

Name Type Description Default
project_settings dict[str, Any]

Project settings.

required
create_context CreateContext

Context which initialized creator.

required
headless bool

Running in headless mode.

False
Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
class BaseCreator(ABC):
    """Plugin that create and modify instance data before publishing process.

    We should maybe find better name as creation is only one part of its logic
    and to avoid expectations that it is the same as `avalon.api.Creator`.

    Single object should be used for multiple instances instead of single
    instance per one creator object. Do not store temp data or mid-process data
    to `self` if it's not Plugin specific.

    Args:
        project_settings (dict[str, Any]): Project settings.
        create_context (CreateContext): Context which initialized creator.
        headless (bool): Running in headless mode.
    """

    # Label shown in UI
    label = None
    group_label = None
    # Cached group label after first call 'get_group_label'
    _cached_group_label = None

    # Order in which will be plugin executed (collect & update instances)
    #   less == earlier -> Order '90' will be processed before '100'
    order = 100

    # Variable to store logger
    _log = None

    # Creator is enabled (Probably does not have reason of existence?)
    enabled = True

    # Creator (and product type) icon
    # - may not be used if `get_icon` is reimplemented
    icon = None

    # Instance attribute definitions that can be changed per instance
    # - returns list of attribute definitions from
    #       `ayon_core.lib.attribute_definitions`
    instance_attr_defs: "list[AbstractAttrDef]" = []

    # Filtering by host name - can be used to be filtered by host name
    # - used on all hosts when set to 'None' for Backwards compatibility
    #   - was added afterwards
    # QUESTION make this required?
    host_name: Optional[str] = None

    # Settings auto-apply helpers
    # Root key in project settings (mandatory for auto-apply to work)
    settings_category: Optional[str] = None
    # Name of plugin in create settings > class name is used if not set
    settings_name: Optional[str] = None

    def __init__(
        self, project_settings, create_context, headless=False
    ):
        # Reference to CreateContext
        self.create_context = create_context
        self.project_settings = project_settings

        # Creator is running in headless mode (without UI elements)
        # - we may use UI inside processing this attribute should be checked
        self.headless = headless

        self.apply_settings(project_settings)
        self.register_callbacks()

    @staticmethod
    def _get_settings_values(project_settings, category_name, plugin_name):
        """Helper method to get settings values.

        Args:
            project_settings (dict[str, Any]): Project settings.
            category_name (str): Category of settings.
            plugin_name (str): Name of settings.

        Returns:
            Optional[dict[str, Any]]: Settings values or None.
        """

        settings = project_settings.get(category_name)
        if not settings:
            return None

        create_settings = settings.get("create")
        if not create_settings:
            return None

        return create_settings.get(plugin_name)

    def apply_settings(self, project_settings):
        """Method called on initialization of plugin to apply settings.

        Default implementation tries to auto-apply settings values if are
            in expected hierarchy.

        Data hierarchy to auto-apply settings:
            ├─ {self.settings_category}                 - Root key in settings
            │ └─ "create"                               - Hardcoded key
            │   └─ {self.settings_name} | {class name}  - Name of plugin
            │     ├─ ... attribute values...            - Attribute/value pair

        It is mandatory to define 'settings_category' attribute. Attribute
        'settings_name' is optional and class name is used if is not defined.

        Example data:
            ProjectSettings {
                "maya": {                    # self.settings_category
                    "create": {              # Hardcoded key
                        "CreateAnimation": { # self.settings_name / class name
                            "enabled": True, # --- Attributes to set ---
                            "optional": True,#
                            "active": True,  #
                            "fps": 25,       # -------------------------
                        },
                        ...
                    },
                    ...
                },
                ...
            }

        Args:
            project_settings (dict[str, Any]): Project settings.
        """

        settings_category = self.settings_category
        if not settings_category:
            return

        cls_name = self.__class__.__name__
        settings_name = self.settings_name or cls_name

        settings = self._get_settings_values(
            project_settings, settings_category, settings_name
        )
        if settings is None:
            self.log.debug("No settings found for {}".format(cls_name))
            return

        for key, value in settings.items():
            # Log out attributes that are not defined on plugin object
            # - those may be potential dangerous typos in settings
            if not hasattr(self, key):
                self.log.debug((
                    "Applying settings to unknown attribute '{}' on '{}'."
                ).format(
                    key, cls_name
                ))
            setattr(self, key, value)

    def register_callbacks(self):
        """Register callbacks for creator.

        Default implementation does nothing. It can be overridden to register
        callbacks for creator.
        """
        pass

    @property
    def identifier(self):
        """Identifier of creator (must be unique).

        Default implementation returns plugin's product type.
        """

        return self.product_type

    @property
    @abstractmethod
    def product_type(self):
        """Family that plugin represents."""

        pass

    @property
    def project_name(self):
        """Current project name.

        Returns:
            str: Name of a project.
        """

        return self.create_context.project_name

    @property
    def project_anatomy(self):
        """Current project anatomy.

        Returns:
            Anatomy: Project anatomy object.
        """

        return self.create_context.project_anatomy

    @property
    def host(self):
        return self.create_context.host

    def get_group_label(self):
        """Group label under which are instances grouped in UI.

        Default implementation use attributes in this order:
            - 'group_label' -> 'label' -> 'identifier'
                Keep in mind that 'identifier' use 'product_type' by default.

        Returns:
            str: Group label that can be used for grouping of instances in UI.
                Group label can be overridden by instance itself.
        """

        if self._cached_group_label is None:
            label = self.identifier
            if self.group_label:
                label = self.group_label
            elif self.label:
                label = self.label
            self._cached_group_label = label
        return self._cached_group_label

    @property
    def log(self):
        """Logger of the plugin.

        Returns:
            logging.Logger: Logger with name of the plugin.
        """

        if self._log is None:
            self._log = Logger.get_logger(self.__class__.__name__)
        return self._log

    def _create_instance(
        self,
        product_name: str,
        data: Dict[str, Any],
        product_type: Optional[str] = None
    ) -> CreatedInstance:
        """Create instance and add instance to context.

        Args:
            product_name (str): Product name.
            data (Dict[str, Any]): Instance data.
            product_type (Optional[str]): Product type, object attribute
                'product_type' is used if not passed.

        Returns:
            CreatedInstance: Created instance.

        """
        if product_type is None:
            product_type = self.product_type
        instance = CreatedInstance(
            product_type,
            product_name,
            data,
            creator=self,
        )
        self._add_instance_to_context(instance)
        return instance

    def _add_instance_to_context(self, instance):
        """Helper method to add instance to create context.

        Instances should be stored to DCC workfile metadata to be able reload
        them and also stored to CreateContext in which is creator plugin
        existing at the moment to be able use it without refresh of
        CreateContext.

        Args:
            instance (CreatedInstance): New created instance.
        """

        self.create_context.creator_adds_instance(instance)

    def _remove_instance_from_context(self, instance):
        """Helper method to remove instance from create context.

        Instances must be removed from DCC workfile metadat aand from create
        context in which plugin is existing at the moment of removal to
        propagate the change without restarting create context.

        Args:
            instance (CreatedInstance): Instance which should be removed.
        """

        self.create_context.creator_removed_instance(instance)

    @abstractmethod
    def create(self):
        """Create new instance.

        Replacement of `process` method from avalon implementation.
        - must expect all data that were passed to init in previous
            implementation
        """

        pass

    @abstractmethod
    def collect_instances(self):
        """Collect existing instances related to this creator plugin.

        The implementation differs on host abilities. The creator has to
        collect metadata about instance and create 'CreatedInstance' object
        which should be added to 'CreateContext'.

        Example:
        ```python
        def collect_instances(self):
            # Getting existing instances is different per host implementation
            for instance_data in pipeline.list_instances():
                # Process only instances that were created by this creator
                creator_id = instance_data.get("creator_identifier")
                if creator_id == self.identifier:
                    # Create instance object from existing data
                    instance = CreatedInstance.from_existing(
                        instance_data, self
                    )
                    # Add instance to create context
                    self._add_instance_to_context(instance)
        ```
        """

        pass

    @abstractmethod
    def update_instances(self, update_list):
        """Store changes of existing instances so they can be recollected.

        Args:
            update_list (list[UpdateData]): Gets list of tuples. Each item
                contain changed instance and it's changes.
        """

        pass

    @abstractmethod
    def remove_instances(self, instances):
        """Method called on instance removal.

        Can also remove instance metadata from context but should return
        'True' if did so.

        Args:
            instances (list[CreatedInstance]): Instance objects which should be
                removed.
        """

        pass

    def get_icon(self):
        """Icon of creator (product type).

        Can return path to image file or awesome icon name.
        """

        return self.icon

    def get_dynamic_data(
        self,
        project_name,
        folder_entity,
        task_entity,
        variant,
        host_name,
        instance
    ):
        """Dynamic data for product name filling.

        These may be dynamically created based on current context of workfile.
        """

        return {}

    def get_product_name(
        self,
        project_name,
        folder_entity,
        task_entity,
        variant,
        host_name=None,
        instance=None,
        project_entity=None,
    ):
        """Return product name for passed context.

        Method is also called on product name update. In that case origin
        instance is passed in.

        Args:
            project_name (str): Project name.
            folder_entity (dict): Folder entity.
            task_entity (dict): Task entity.
            variant (str): Product name variant. In most of cases user input.
            host_name (Optional[str]): Which host creates product. Defaults
                to host name on create context.
            instance (Optional[CreatedInstance]): Object of 'CreatedInstance'
                for which is product name updated. Passed only on product name
                update.
            project_entity (Optional[dict[str, Any]]): Project entity.

        """
        if host_name is None:
            host_name = self.create_context.host_name

        task_name = task_type = None
        if task_entity:
            task_name = task_entity["name"]
            task_type = task_entity["taskType"]

        dynamic_data = self.get_dynamic_data(
            project_name,
            folder_entity,
            task_entity,
            variant,
            host_name,
            instance
        )

        cur_project_name = self.create_context.get_current_project_name()
        if not project_entity and project_name == cur_project_name:
            project_entity = self.create_context.get_current_project_entity()

        return get_product_name(
            project_name,
            task_name,
            task_type,
            host_name,
            self.product_type,
            variant,
            dynamic_data=dynamic_data,
            project_settings=self.project_settings,
            project_entity=project_entity,
        )

    def get_instance_attr_defs(self):
        """Plugin attribute definitions.

        Attribute definitions of plugin that hold data about created instance
        and values are stored to metadata for future usage and for publishing
        purposes.

        NOTE:
        Convert method should be implemented which should care about updating
        keys/values when plugin attributes change.

        Returns:
            list[AbstractAttrDef]: Attribute definitions that can be tweaked
                for created instance.
        """

        return self.instance_attr_defs

    def get_attr_defs_for_instance(self, instance):
        """Get attribute definitions for an instance.

        Args:
            instance (CreatedInstance): Instance for which to get
                attribute definitions.

        """
        return self.get_instance_attr_defs()

    @property
    def collection_shared_data(self):
        """Access to shared data that can be used during creator's collection.

        Returns:
            dict[str, Any]: Shared data.

        Raises:
            UnavailableSharedData: When called out of collection phase.
        """

        return self.create_context.collection_shared_data

    def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None):
        """Set path to thumbnail for instance."""

        self.create_context.thumbnail_paths_by_instance_id[instance_id] = (
            thumbnail_path
        )

    def get_next_versions_for_instances(self, instances):
        """Prepare next versions for instances.

        This is helper method to receive next possible versions for instances.
        It is using context information on instance to receive them,
        'folderPath' and 'product'.

        Output will contain version by each instance id.

        Args:
            instances (list[CreatedInstance]): Instances for which to get next
                versions.

        Returns:
            dict[str, int]: Next versions by instance id.
        """

        return get_next_versions_for_instances(
            self.create_context.project_name, instances
        )

collection_shared_data property

Access to shared data that can be used during creator's collection.

Returns:

Type Description

dict[str, Any]: Shared data.

Raises:

Type Description
UnavailableSharedData

When called out of collection phase.

identifier property

Identifier of creator (must be unique).

Default implementation returns plugin's product type.

log property

Logger of the plugin.

Returns:

Type Description

logging.Logger: Logger with name of the plugin.

product_type abstractmethod property

Family that plugin represents.

project_anatomy property

Current project anatomy.

Returns:

Name Type Description
Anatomy

Project anatomy object.

project_name property

Current project name.

Returns:

Name Type Description
str

Name of a project.

apply_settings(project_settings)

Method called on initialization of plugin to apply settings.

Default implementation tries to auto-apply settings values if are in expected hierarchy.

Data hierarchy to auto-apply settings

├─ {self.settings_category} - Root key in settings │ └─ "create" - Hardcoded key │ └─ {self.settings_name} | {class name} - Name of plugin │ ├─ ... attribute values... - Attribute/value pair

It is mandatory to define 'settings_category' attribute. Attribute 'settings_name' is optional and class name is used if is not defined.

Example data

ProjectSettings { "maya": { # self.settings_category "create": { # Hardcoded key "CreateAnimation": { # self.settings_name / class name "enabled": True, # --- Attributes to set --- "optional": True,# "active": True, # "fps": 25, # ------------------------- }, ... }, ... }, ... }

Parameters:

Name Type Description Default
project_settings dict[str, Any]

Project settings.

required
Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
def apply_settings(self, project_settings):
    """Method called on initialization of plugin to apply settings.

    Default implementation tries to auto-apply settings values if are
        in expected hierarchy.

    Data hierarchy to auto-apply settings:
        ├─ {self.settings_category}                 - Root key in settings
        │ └─ "create"                               - Hardcoded key
        │   └─ {self.settings_name} | {class name}  - Name of plugin
        │     ├─ ... attribute values...            - Attribute/value pair

    It is mandatory to define 'settings_category' attribute. Attribute
    'settings_name' is optional and class name is used if is not defined.

    Example data:
        ProjectSettings {
            "maya": {                    # self.settings_category
                "create": {              # Hardcoded key
                    "CreateAnimation": { # self.settings_name / class name
                        "enabled": True, # --- Attributes to set ---
                        "optional": True,#
                        "active": True,  #
                        "fps": 25,       # -------------------------
                    },
                    ...
                },
                ...
            },
            ...
        }

    Args:
        project_settings (dict[str, Any]): Project settings.
    """

    settings_category = self.settings_category
    if not settings_category:
        return

    cls_name = self.__class__.__name__
    settings_name = self.settings_name or cls_name

    settings = self._get_settings_values(
        project_settings, settings_category, settings_name
    )
    if settings is None:
        self.log.debug("No settings found for {}".format(cls_name))
        return

    for key, value in settings.items():
        # Log out attributes that are not defined on plugin object
        # - those may be potential dangerous typos in settings
        if not hasattr(self, key):
            self.log.debug((
                "Applying settings to unknown attribute '{}' on '{}'."
            ).format(
                key, cls_name
            ))
        setattr(self, key, value)

collect_instances() abstractmethod

Collect existing instances related to this creator plugin.

The implementation differs on host abilities. The creator has to collect metadata about instance and create 'CreatedInstance' object which should be added to 'CreateContext'.

Example:

def collect_instances(self):
    # Getting existing instances is different per host implementation
    for instance_data in pipeline.list_instances():
        # Process only instances that were created by this creator
        creator_id = instance_data.get("creator_identifier")
        if creator_id == self.identifier:
            # Create instance object from existing data
            instance = CreatedInstance.from_existing(
                instance_data, self
            )
            # Add instance to create context
            self._add_instance_to_context(instance)
Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
@abstractmethod
def collect_instances(self):
    """Collect existing instances related to this creator plugin.

    The implementation differs on host abilities. The creator has to
    collect metadata about instance and create 'CreatedInstance' object
    which should be added to 'CreateContext'.

    Example:
    ```python
    def collect_instances(self):
        # Getting existing instances is different per host implementation
        for instance_data in pipeline.list_instances():
            # Process only instances that were created by this creator
            creator_id = instance_data.get("creator_identifier")
            if creator_id == self.identifier:
                # Create instance object from existing data
                instance = CreatedInstance.from_existing(
                    instance_data, self
                )
                # Add instance to create context
                self._add_instance_to_context(instance)
    ```
    """

    pass

create() abstractmethod

Create new instance.

Replacement of process method from avalon implementation. - must expect all data that were passed to init in previous implementation

Source code in client/ayon_core/pipeline/create/creator_plugins.py
433
434
435
436
437
438
439
440
441
442
@abstractmethod
def create(self):
    """Create new instance.

    Replacement of `process` method from avalon implementation.
    - must expect all data that were passed to init in previous
        implementation
    """

    pass

get_attr_defs_for_instance(instance)

Get attribute definitions for an instance.

Parameters:

Name Type Description Default
instance CreatedInstance

Instance for which to get attribute definitions.

required
Source code in client/ayon_core/pipeline/create/creator_plugins.py
599
600
601
602
603
604
605
606
607
def get_attr_defs_for_instance(self, instance):
    """Get attribute definitions for an instance.

    Args:
        instance (CreatedInstance): Instance for which to get
            attribute definitions.

    """
    return self.get_instance_attr_defs()

get_dynamic_data(project_name, folder_entity, task_entity, variant, host_name, instance)

Dynamic data for product name filling.

These may be dynamically created based on current context of workfile.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
def get_dynamic_data(
    self,
    project_name,
    folder_entity,
    task_entity,
    variant,
    host_name,
    instance
):
    """Dynamic data for product name filling.

    These may be dynamically created based on current context of workfile.
    """

    return {}

get_group_label()

Group label under which are instances grouped in UI.

Default implementation use attributes in this order
  • 'group_label' -> 'label' -> 'identifier' Keep in mind that 'identifier' use 'product_type' by default.

Returns:

Name Type Description
str

Group label that can be used for grouping of instances in UI. Group label can be overridden by instance itself.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def get_group_label(self):
    """Group label under which are instances grouped in UI.

    Default implementation use attributes in this order:
        - 'group_label' -> 'label' -> 'identifier'
            Keep in mind that 'identifier' use 'product_type' by default.

    Returns:
        str: Group label that can be used for grouping of instances in UI.
            Group label can be overridden by instance itself.
    """

    if self._cached_group_label is None:
        label = self.identifier
        if self.group_label:
            label = self.group_label
        elif self.label:
            label = self.label
        self._cached_group_label = label
    return self._cached_group_label

get_icon()

Icon of creator (product type).

Can return path to image file or awesome icon name.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
496
497
498
499
500
501
502
def get_icon(self):
    """Icon of creator (product type).

    Can return path to image file or awesome icon name.
    """

    return self.icon

get_instance_attr_defs()

Plugin attribute definitions.

Attribute definitions of plugin that hold data about created instance and values are stored to metadata for future usage and for publishing purposes.

NOTE: Convert method should be implemented which should care about updating keys/values when plugin attributes change.

Returns:

Type Description

list[AbstractAttrDef]: Attribute definitions that can be tweaked for created instance.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
def get_instance_attr_defs(self):
    """Plugin attribute definitions.

    Attribute definitions of plugin that hold data about created instance
    and values are stored to metadata for future usage and for publishing
    purposes.

    NOTE:
    Convert method should be implemented which should care about updating
    keys/values when plugin attributes change.

    Returns:
        list[AbstractAttrDef]: Attribute definitions that can be tweaked
            for created instance.
    """

    return self.instance_attr_defs

get_next_versions_for_instances(instances)

Prepare next versions for instances.

This is helper method to receive next possible versions for instances. It is using context information on instance to receive them, 'folderPath' and 'product'.

Output will contain version by each instance id.

Parameters:

Name Type Description Default
instances list[CreatedInstance]

Instances for which to get next versions.

required

Returns:

Type Description

dict[str, int]: Next versions by instance id.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
def get_next_versions_for_instances(self, instances):
    """Prepare next versions for instances.

    This is helper method to receive next possible versions for instances.
    It is using context information on instance to receive them,
    'folderPath' and 'product'.

    Output will contain version by each instance id.

    Args:
        instances (list[CreatedInstance]): Instances for which to get next
            versions.

    Returns:
        dict[str, int]: Next versions by instance id.
    """

    return get_next_versions_for_instances(
        self.create_context.project_name, instances
    )

get_product_name(project_name, folder_entity, task_entity, variant, host_name=None, instance=None, project_entity=None)

Return product name for passed context.

Method is also called on product name update. In that case origin instance is passed in.

Parameters:

Name Type Description Default
project_name str

Project name.

required
folder_entity dict

Folder entity.

required
task_entity dict

Task entity.

required
variant str

Product name variant. In most of cases user input.

required
host_name Optional[str]

Which host creates product. Defaults to host name on create context.

None
instance Optional[CreatedInstance]

Object of 'CreatedInstance' for which is product name updated. Passed only on product name update.

None
project_entity Optional[dict[str, Any]]

Project entity.

None
Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
def get_product_name(
    self,
    project_name,
    folder_entity,
    task_entity,
    variant,
    host_name=None,
    instance=None,
    project_entity=None,
):
    """Return product name for passed context.

    Method is also called on product name update. In that case origin
    instance is passed in.

    Args:
        project_name (str): Project name.
        folder_entity (dict): Folder entity.
        task_entity (dict): Task entity.
        variant (str): Product name variant. In most of cases user input.
        host_name (Optional[str]): Which host creates product. Defaults
            to host name on create context.
        instance (Optional[CreatedInstance]): Object of 'CreatedInstance'
            for which is product name updated. Passed only on product name
            update.
        project_entity (Optional[dict[str, Any]]): Project entity.

    """
    if host_name is None:
        host_name = self.create_context.host_name

    task_name = task_type = None
    if task_entity:
        task_name = task_entity["name"]
        task_type = task_entity["taskType"]

    dynamic_data = self.get_dynamic_data(
        project_name,
        folder_entity,
        task_entity,
        variant,
        host_name,
        instance
    )

    cur_project_name = self.create_context.get_current_project_name()
    if not project_entity and project_name == cur_project_name:
        project_entity = self.create_context.get_current_project_entity()

    return get_product_name(
        project_name,
        task_name,
        task_type,
        host_name,
        self.product_type,
        variant,
        dynamic_data=dynamic_data,
        project_settings=self.project_settings,
        project_entity=project_entity,
    )

register_callbacks()

Register callbacks for creator.

Default implementation does nothing. It can be overridden to register callbacks for creator.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
296
297
298
299
300
301
302
def register_callbacks(self):
    """Register callbacks for creator.

    Default implementation does nothing. It can be overridden to register
    callbacks for creator.
    """
    pass

remove_instances(instances) abstractmethod

Method called on instance removal.

Can also remove instance metadata from context but should return 'True' if did so.

Parameters:

Name Type Description Default
instances list[CreatedInstance]

Instance objects which should be removed.

required
Source code in client/ayon_core/pipeline/create/creator_plugins.py
482
483
484
485
486
487
488
489
490
491
492
493
494
@abstractmethod
def remove_instances(self, instances):
    """Method called on instance removal.

    Can also remove instance metadata from context but should return
    'True' if did so.

    Args:
        instances (list[CreatedInstance]): Instance objects which should be
            removed.
    """

    pass

set_instance_thumbnail_path(instance_id, thumbnail_path=None)

Set path to thumbnail for instance.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
622
623
624
625
626
627
def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None):
    """Set path to thumbnail for instance."""

    self.create_context.thumbnail_paths_by_instance_id[instance_id] = (
        thumbnail_path
    )

update_instances(update_list) abstractmethod

Store changes of existing instances so they can be recollected.

Parameters:

Name Type Description Default
update_list list[UpdateData]

Gets list of tuples. Each item contain changed instance and it's changes.

required
Source code in client/ayon_core/pipeline/create/creator_plugins.py
471
472
473
474
475
476
477
478
479
480
@abstractmethod
def update_instances(self, update_list):
    """Store changes of existing instances so they can be recollected.

    Args:
        update_list (list[UpdateData]): Gets list of tuples. Each item
            contain changed instance and it's changes.
    """

    pass

ConvertorItem

Item representing convertor plugin.

Parameters:

Name Type Description Default
identifier str

Identifier of convertor.

required
label str

Label which will be shown in UI.

required
Source code in client/ayon_core/pipeline/create/structures.py
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
class ConvertorItem:
    """Item representing convertor plugin.

    Args:
        identifier (str): Identifier of convertor.
        label (str): Label which will be shown in UI.
    """

    def __init__(self, identifier, label):
        self._id = str(uuid4())
        self.identifier = identifier
        self.label = label

    @property
    def id(self):
        return self._id

    def to_data(self):
        return {
            "id": self.id,
            "identifier": self.identifier,
            "label": self.label
        }

    @classmethod
    def from_data(cls, data):
        obj = cls(data["identifier"], data["label"])
        obj._id = data["id"]
        return obj

CreateContext

Context of instance creation.

Context itself also can store data related to whole creation (workfile). - those are mainly for Context publish plugins

Todos

Don't use 'AvalonMongoDB'. It's used only to keep track about current context which should be handled by host.

Parameters:

Name Type Description Default
host(ModuleType)

Host implementation which handles implementation and global metadata.

required
headless(bool)

Context is created out of UI (Current not used).

required
reset(bool)

Reset context on initialization.

required
discover_publish_plugins(bool)

Discover publish plugins during reset phase.

required
Source code in client/ayon_core/pipeline/create/context.py
 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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
class CreateContext:
    """Context of instance creation.

    Context itself also can store data related to whole creation (workfile).
    - those are mainly for Context publish plugins

    Todos:
        Don't use 'AvalonMongoDB'. It's used only to keep track about current
            context which should be handled by host.

    Args:
        host(ModuleType): Host implementation which handles implementation and
            global metadata.
        headless(bool): Context is created out of UI (Current not used).
        reset(bool): Reset context on initialization.
        discover_publish_plugins(bool): Discover publish plugins during reset
            phase.
    """

    def __init__(
        self, host, headless=False, reset=True, discover_publish_plugins=True
    ):
        self.host = host

        # Prepare attribute for logger (Created on demand in `log` property)
        self._log = None
        self._event_hub = QueuedEventSystem()

        # Publish context plugins attributes and it's values
        self._publish_attributes = PublishAttributes(self, {})
        self._original_context_data = {}

        # Validate host implementation
        # - defines if context is capable of handling context data
        host_is_valid = True
        missing_methods = self.get_host_misssing_methods(host)
        if missing_methods:
            host_is_valid = False
            joined_methods = ", ".join(
                ['"{}"'.format(name) for name in missing_methods]
            )
            self.log.warning((
                "Host miss required methods to be able use creation."
                " Missing methods: {}"
            ).format(joined_methods))

        self._current_project_name = None
        self._current_folder_path = None
        self._current_task_name = None
        self._current_workfile_path = None
        self._current_project_settings = None

        self._current_project_entity = _NOT_SET
        self._current_folder_entity = _NOT_SET
        self._current_task_entity = _NOT_SET
        self._current_task_type = _NOT_SET

        self._current_project_anatomy = None

        self._host_is_valid = host_is_valid
        # Currently unused variable
        self.headless = headless

        # Instances by their ID
        self._instances_by_id = {}

        self.creator_discover_result = None
        self.convertor_discover_result = None
        # Discovered creators
        self.creators = {}
        # Prepare categories of creators
        self.autocreators = {}
        # Manual creators
        self.manual_creators = {}
        # Creators that are disabled
        self.disabled_creators = {}

        self.convertors_plugins = {}
        self.convertor_items_by_id = {}

        self.publish_discover_result: Optional[DiscoverResult] = None
        self.publish_plugins_mismatch_targets = []
        self.publish_plugins = []
        self.plugins_with_defs = []

        # Helpers for validating context of collected instances
        #   - they can be validation for multiple instances at one time
        #       using context manager which will trigger validation
        #       after leaving of last context manager scope
        self._bulk_info = {
            # Added instances
            "add": BulkInfo(),
            # Removed instances
            "remove": BulkInfo(),
            # Change values of instances or create context
            "change": BulkInfo(),
            # Pre create attribute definitions changed
            "pre_create_attrs_change": BulkInfo(),
            # Create attribute definitions changed
            "create_attrs_change": BulkInfo(),
            # Publish attribute definitions changed
            "publish_attrs_change": BulkInfo(),
        }
        self._bulk_order = []

        # Shared data across creators during collection phase
        self._collection_shared_data = None

        # Entities cache
        self._folder_entities_by_path = {}
        self._task_entities_by_id = {}
        self._task_ids_by_folder_path = {}
        self._task_names_by_folder_path = {}

        self.thumbnail_paths_by_instance_id = {}

        # Trigger reset if was enabled
        if reset:
            self.reset(discover_publish_plugins)

    @property
    def instances(self):
        return self._instances_by_id.values()

    @property
    def instances_by_id(self):
        return self._instances_by_id

    @property
    def publish_attributes(self):
        """Access to global publish attributes."""
        return self._publish_attributes

    def get_instance_by_id(
        self, instance_id: str
    ) -> Optional["CreatedInstance"]:
        """Receive instance by id.

        Args:
            instance_id (str): Instance id.

        Returns:
            Optional[CreatedInstance]: Instance or None if instance with
                given id is not available.

        """
        return self._instances_by_id.get(instance_id)

    def get_sorted_creators(self, identifiers=None):
        """Sorted creators by 'order' attribute.

        Args:
            identifiers (Iterable[str]): Filter creators by identifiers. All
                creators are returned if 'None' is passed.

        Returns:
            List[BaseCreator]: Sorted creator plugins by 'order' value.

        """
        if identifiers is not None:
            identifiers = set(identifiers)
            creators = [
                creator
                for identifier, creator in self.creators.items()
                if identifier in identifiers
            ]
        else:
            creators = self.creators.values()

        return sorted(
            creators, key=lambda creator: creator.order
        )

    @property
    def sorted_creators(self):
        """Sorted creators by 'order' attribute.

        Returns:
            List[BaseCreator]: Sorted creator plugins by 'order' value.
        """

        return self.get_sorted_creators()

    @property
    def sorted_autocreators(self):
        """Sorted auto-creators by 'order' attribute.

        Returns:
            List[AutoCreator]: Sorted plugins by 'order' value.
        """

        return sorted(
            self.autocreators.values(), key=lambda creator: creator.order
        )

    @classmethod
    def get_host_misssing_methods(cls, host):
        """Collect missing methods from host.

        Args:
            host(ModuleType): Host implementaion.
        """

        missing = set(
            IPublishHost.get_missing_publish_methods(host)
        )
        return missing

    @property
    def host_is_valid(self):
        """Is host valid for creation."""
        return self._host_is_valid

    @property
    def host_name(self) -> str:
        if hasattr(self.host, "name"):
            return self.host.name
        return os.environ["AYON_HOST_NAME"]

    def get_current_project_name(self) -> Optional[str]:
        """Project name which was used as current context on context reset.

        Returns:
            Union[str, None]: Project name.
        """

        return self._current_project_name

    def get_current_folder_path(self) -> Optional[str]:
        """Folder path which was used as current context on context reset.

        Returns:
            Union[str, None]: Folder path.
        """

        return self._current_folder_path

    def get_current_task_name(self) -> Optional[str]:
        """Task name which was used as current context on context reset.

        Returns:
            Union[str, None]: Task name.
        """

        return self._current_task_name

    def get_current_task_type(self) -> Optional[str]:
        """Task type which was used as current context on context reset.

        Returns:
            Union[str, None]: Task type.

        """
        if self._current_task_type is _NOT_SET:
            task_type = None
            task_entity = self.get_current_task_entity()
            if task_entity:
                task_type = task_entity["taskType"]
            self._current_task_type = task_type
        return self._current_task_type

    def get_current_project_entity(self) -> Optional[Dict[str, Any]]:
        """Project entity for current context project.

        Returns:
            Union[dict[str, Any], None]: Folder entity.

        """
        if self._current_project_entity is not _NOT_SET:
            return copy.deepcopy(self._current_project_entity)
        project_entity = None
        project_name = self.get_current_project_name()
        if project_name:
            project_entity = ayon_api.get_project(project_name)
        self._current_project_entity = project_entity
        return copy.deepcopy(self._current_project_entity)

    def get_current_folder_entity(self) -> Optional[Dict[str, Any]]:
        """Folder entity for current context folder.

        Returns:
            Optional[dict[str, Any]]: Folder entity.

        """
        if self._current_folder_entity is not _NOT_SET:
            return copy.deepcopy(self._current_folder_entity)

        folder_path = self.get_current_folder_path()
        self._current_folder_entity = self.get_folder_entity(folder_path)
        return copy.deepcopy(self._current_folder_entity)

    def get_current_task_entity(self) -> Optional[Dict[str, Any]]:
        """Task entity for current context task.

        Returns:
            Union[dict[str, Any], None]: Task entity.

        """
        if self._current_task_entity is not _NOT_SET:
            return copy.deepcopy(self._current_task_entity)

        folder_path = self.get_current_folder_path()
        task_name = self.get_current_task_name()
        self._current_task_entity = self.get_task_entity(
            folder_path, task_name
        )
        return copy.deepcopy(self._current_task_entity)

    def get_current_workfile_path(self):
        """Workfile path which was opened on context reset.

        Returns:
            Union[str, None]: Workfile path.
        """

        return self._current_workfile_path

    def get_current_project_anatomy(self):
        """Project anatomy for current project.

        Returns:
            Anatomy: Anatomy object ready to be used.
        """

        if self._current_project_anatomy is None:
            self._current_project_anatomy = Anatomy(
                self._current_project_name)
        return self._current_project_anatomy

    def get_current_project_settings(self):
        if self._current_project_settings is None:
            self._current_project_settings = get_project_settings(
                self.get_current_project_name())
        return self._current_project_settings

    def get_template_data(
        self, folder_path: Optional[str], task_name: Optional[str]
    ) -> Dict[str, Any]:
        """Prepare template data for given context.

        Method is using cached entities and settings to prepare template data.

        Args:
            folder_path (Optional[str]): Folder path.
            task_name (Optional[str]): Task name.

        Returns:
            dict[str, Any]: Template data.

        """
        project_entity = self.get_current_project_entity()
        folder_entity = task_entity = None
        if folder_path:
            folder_entity = self.get_folder_entity(folder_path)
            if task_name and folder_entity:
                task_entity = self.get_task_entity(folder_path, task_name)

        return get_template_data(
            project_entity,
            folder_entity,
            task_entity,
            host_name=self.host_name,
            settings=self.get_current_project_settings(),
        )

    @property
    def context_has_changed(self):
        """Host context has changed.

        As context is used project, folder, task name and workfile path if
        host does support workfiles.

        Returns:
            bool: Context changed.
        """

        project_name, folder_path, task_name, workfile_path = (
            self._get_current_host_context()
        )
        return (
            self._current_project_name != project_name
            or self._current_folder_path != folder_path
            or self._current_task_name != task_name
            or self._current_workfile_path != workfile_path
        )

    project_name = property(get_current_project_name)
    project_anatomy = property(get_current_project_anatomy)

    @property
    def log(self):
        """Dynamic access to logger."""
        if self._log is None:
            self._log = logging.getLogger(self.__class__.__name__)
        return self._log

    def reset(self, discover_publish_plugins=True):
        """Reset context with all plugins and instances.

        All changes will be lost if were not saved explicitely.
        """

        self.reset_preparation()

        self.reset_current_context()
        self.reset_plugins(discover_publish_plugins)
        self.reset_context_data()

        with self.bulk_add_instances():
            self.reset_instances()
            self.find_convertor_items()
            self.execute_autocreators()

        self.reset_finalization()

    def refresh_thumbnails(self):
        """Cleanup thumbnail paths.

        Remove all thumbnail filepaths that are empty or lead to files which
        does not exist or of instances that are not available anymore.
        """

        invalid = set()
        for instance_id, path in self.thumbnail_paths_by_instance_id.items():
            instance_available = True
            if instance_id is not None:
                instance_available = instance_id in self._instances_by_id

            if (
                not instance_available
                or not path
                or not os.path.exists(path)
            ):
                invalid.add(instance_id)

        for instance_id in invalid:
            self.thumbnail_paths_by_instance_id.pop(instance_id)

    def reset_preparation(self):
        """Prepare attributes that must be prepared/cleaned before reset."""

        # Give ability to store shared data for collection phase
        self._collection_shared_data = {}

        self._folder_entities_by_path = {}
        self._task_entities_by_id = {}

        self._task_ids_by_folder_path = {}
        self._task_names_by_folder_path = {}

        self._event_hub.clear_callbacks()

    def reset_finalization(self):
        """Cleanup of attributes after reset."""

        # Stop access to collection shared data
        self._collection_shared_data = None
        self.refresh_thumbnails()

    def _get_current_host_context(self):
        project_name = folder_path = task_name = workfile_path = None
        if hasattr(self.host, "get_current_context"):
            host_context = self.host.get_current_context()
            if host_context:
                project_name = host_context.get("project_name")
                folder_path = host_context.get("folder_path")
                task_name = host_context.get("task_name")

        if isinstance(self.host, IWorkfileHost):
            workfile_path = self.host.get_current_workfile()

        return project_name, folder_path, task_name, workfile_path

    def reset_current_context(self):
        """Refresh current context.

        Reset is based on optional host implementation of `get_current_context`
        function.

        Some hosts have ability to change context file without using workfiles
        tool but that change is not propagated to 'os.environ'.

        Todos:
            UI: Current context should be also checked on save - compare
                initial values vs. current values.
            Related to UI checks: Current workfile can be also considered
                as current context information as that's where the metadata
                are stored. We should store the workfile (if is available) too.
        """

        project_name, folder_path, task_name, workfile_path = (
            self._get_current_host_context()
        )

        self._current_project_name = project_name
        self._current_folder_path = folder_path
        self._current_task_name = task_name
        self._current_workfile_path = workfile_path

        self._current_project_entity = _NOT_SET
        self._current_folder_entity = _NOT_SET
        self._current_task_entity = _NOT_SET
        self._current_task_type = _NOT_SET

        self._current_project_anatomy = None
        self._current_project_settings = None

    def reset_plugins(self, discover_publish_plugins=True):
        """Reload plugins.

        Reloads creators from preregistered paths and can load publish plugins
        if it's enabled on context.
        """

        self._reset_publish_plugins(discover_publish_plugins)
        self._reset_creator_plugins()
        self._reset_convertor_plugins()

    def _reset_publish_plugins(self, discover_publish_plugins):
        from ayon_core.pipeline import AYONPyblishPluginMixin
        from ayon_core.pipeline.publish import (
            publish_plugins_discover
        )

        discover_result = DiscoverResult(pyblish.api.Plugin)
        plugins_with_defs = []
        plugins_by_targets = []
        plugins_mismatch_targets = []
        if discover_publish_plugins:
            discover_result = publish_plugins_discover()
            publish_plugins = discover_result.plugins

            targets = set(pyblish.logic.registered_targets())
            targets.add("default")
            plugins_by_targets = pyblish.logic.plugins_by_targets(
                publish_plugins, list(targets)
            )

            # Collect plugins that can have attribute definitions
            for plugin in publish_plugins:
                if AYONPyblishPluginMixin in inspect.getmro(plugin):
                    plugins_with_defs.append(plugin)

            plugins_mismatch_targets = [
                plugin
                for plugin in publish_plugins
                if plugin not in plugins_by_targets
            ]

        # Register create context callbacks
        for plugin in plugins_with_defs:
            if not inspect.ismethod(plugin.register_create_context_callbacks):
                self.log.warning(
                    f"Plugin {plugin.__name__} does not have"
                    f" 'register_create_context_callbacks'"
                    f" defined as class method."
                )
                continue
            try:
                plugin.register_create_context_callbacks(self)
            except Exception:
                self.log.error(
                    f"Failed to register callbacks for plugin"
                    f" {plugin.__name__}.",
                    exc_info=True
                )

        self.publish_plugins_mismatch_targets = plugins_mismatch_targets
        self.publish_discover_result = discover_result
        self.publish_plugins = plugins_by_targets
        self.plugins_with_defs = plugins_with_defs

    def _reset_creator_plugins(self):
        # Prepare settings
        project_settings = self.get_current_project_settings()

        # Discover and prepare creators
        creators = {}
        disabled_creators = {}
        autocreators = {}
        manual_creators = {}
        report = discover_creator_plugins(return_report=True)
        self.creator_discover_result = report
        for creator_class in report.plugins:
            if inspect.isabstract(creator_class):
                self.log.debug(
                    "Skipping abstract Creator {}".format(str(creator_class))
                )
                continue

            creator_identifier = creator_class.identifier
            if creator_identifier in creators:
                self.log.warning(
                    "Duplicate Creator identifier: '%s'. Using first Creator "
                    "and skipping: %s", creator_identifier, creator_class
                )
                continue

            # Filter by host name
            if (
                creator_class.host_name
                and creator_class.host_name != self.host_name
            ):
                self.log.info((
                    "Creator's host name \"{}\""
                    " is not supported for current host \"{}\""
                ).format(creator_class.host_name, self.host_name))
                continue

            # TODO report initialization error
            try:
                creator = creator_class(
                    project_settings,
                    self,
                    self.headless
                )
            except Exception:
                self.log.error(
                    f"Failed to initialize plugin: {creator_class}",
                    exc_info=True
                )
                continue

            if not creator.enabled:
                disabled_creators[creator_identifier] = creator
                continue
            creators[creator_identifier] = creator
            if isinstance(creator, AutoCreator):
                autocreators[creator_identifier] = creator
            elif isinstance(creator, Creator):
                manual_creators[creator_identifier] = creator

        self.autocreators = autocreators
        self.manual_creators = manual_creators

        self.creators = creators
        self.disabled_creators = disabled_creators

    def _reset_convertor_plugins(self):
        convertors_plugins = {}
        report = discover_convertor_plugins(return_report=True)
        self.convertor_discover_result = report
        for convertor_class in report.plugins:
            if inspect.isabstract(convertor_class):
                self.log.info(
                    "Skipping abstract Creator {}".format(str(convertor_class))
                )
                continue

            convertor_identifier = convertor_class.identifier
            if convertor_identifier in convertors_plugins:
                self.log.warning((
                    "Duplicated Converter identifier. "
                    "Using first and skipping following"
                ))
                continue

            convertors_plugins[convertor_identifier] = convertor_class(self)

        self.convertors_plugins = convertors_plugins

    def reset_context_data(self):
        """Reload context data using host implementation.

        These data are not related to any instance but may be needed for whole
        publishing.
        """
        if not self.host_is_valid:
            self._original_context_data = {}
            self._publish_attributes = PublishAttributes(self, {})
            return

        original_data = self.host.get_context_data() or {}
        self._original_context_data = copy.deepcopy(original_data)

        publish_attributes = original_data.get("publish_attributes") or {}

        self._publish_attributes = PublishAttributes(
            self, publish_attributes
        )

        for plugin in self.plugins_with_defs:
            if is_func_signature_supported(
                plugin.convert_attribute_values, self, None
            ):
                plugin.convert_attribute_values(self, None)

            elif not plugin.__instanceEnabled__:
                output = plugin.convert_attribute_values(publish_attributes)
                if output:
                    publish_attributes.update(output)

        for plugin in self.plugins_with_defs:
            attr_defs = plugin.get_attr_defs_for_context(self)
            if not attr_defs:
                continue
            self._publish_attributes.set_publish_plugin_attr_defs(
                plugin.__name__, attr_defs
            )

    def add_instances_added_callback(self, callback):
        """Register callback for added instances.

        Event is triggered when instances are already available in context
            and have set create/publish attribute definitions.

        Data structure of event::

            ```python
            {
                "instances": [CreatedInstance, ...],
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                instances are added to context.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        return self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback)

    def add_instances_removed_callback (self, callback):
        """Register callback for removed instances.

        Event is triggered when instances are already removed from context.

        Data structure of event::

            ```python
            {
                "instances": [CreatedInstance, ...],
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                instances are removed from context.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback)

    def add_value_changed_callback(self, callback):
        """Register callback to listen value changes.

        Event is triggered when any value changes on any instance or
            context data.

        Data structure of event::

            ```python
            {
                "changes": [
                    {
                        "instance": CreatedInstance,
                        "changes": {
                            "folderPath": "/new/folder/path",
                            "creator_attributes": {
                                "attr_1": "value_1"
                            }
                        }
                    }
                ],
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                value changed.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback)

    def add_pre_create_attr_defs_change_callback (self, callback):
        """Register callback to listen pre-create attribute changes.

        Create plugin can trigger refresh of pre-create attributes. Usage of
            this event is mainly for publisher UI.

        Data structure of event::

            ```python
            {
                "identifiers": ["create_plugin_identifier"],
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                pre-create attributes should be refreshed.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        self._event_hub.add_callback(
            PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback
        )

    def add_create_attr_defs_change_callback (self, callback):
        """Register callback to listen create attribute changes.

        Create plugin changed attribute definitions of instance.

        Data structure of event::

            ```python
            {
                "instances": [CreatedInstance, ...],
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                create attributes changed.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback)

    def add_publish_attr_defs_change_callback (self, callback):
        """Register callback to listen publish attribute changes.

        Publish plugin changed attribute definitions of instance of context.

        Data structure of event::

            ```python
            {
                "instance_changes": {
                    None: {
                        "instance": None,
                        "plugin_names": {"PluginA"},
                    }
                    "<instance_id>": {
                        "instance": CreatedInstance,
                        "plugin_names": {"PluginB", "PluginC"},
                    }
                },
                "create_context": CreateContext
            }
            ```

        Args:
            callback (Callable): Callback function that will be called when
                publish attributes changed.

        Returns:
            EventCallback: Created callback object which can be used to
                stop listening.

        """
        self._event_hub.add_callback(
            PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback
        )

    def context_data_to_store(self):
        """Data that should be stored by host function.

        The same data should be returned on loading.
        """
        return {
            "publish_attributes": self._publish_attributes.data_to_store()
        }

    def context_data_changes(self):
        """Changes of attributes."""

        return TrackChangesItem(
            self._original_context_data, self.context_data_to_store()
        )

    def set_context_publish_plugin_attr_defs(self, plugin_name, attr_defs):
        """Set attribute definitions for CreateContext publish plugin.

        Args:
            plugin_name(str): Name of publish plugin.
            attr_defs(List[AbstractAttrDef]): Attribute definitions.

        """
        self.publish_attributes.set_publish_plugin_attr_defs(
            plugin_name, attr_defs
        )
        self.instance_publish_attr_defs_changed(
            None, plugin_name
        )

    def creator_adds_instance(self, instance: "CreatedInstance"):
        """Creator adds new instance to context.

        Instances should be added only from creators.

        Args:
            instance(CreatedInstance): Instance with prepared data from
                creator.

        TODO: Rename method to more suit.
        """
        # Add instance to instances list
        if instance.id in self._instances_by_id:
            self.log.warning((
                "Instance with id {} is already added to context."
            ).format(instance.id))
            return

        self._instances_by_id[instance.id] = instance

        # Add instance to be validated inside 'bulk_add_instances'
        #   context manager if is inside bulk
        with self.bulk_add_instances() as bulk_info:
            bulk_info.append(instance)

    def _get_creator_in_create(self, identifier):
        """Creator by identifier with unified error.

        Helper method to get creator by identifier with same error when creator
        is not available.

        Args:
            identifier (str): Identifier of creator plugin.

        Returns:
            BaseCreator: Creator found by identifier.

        Raises:
            CreatorError: When identifier is not known.
        """

        creator = self.creators.get(identifier)
        # Fake CreatorError (Could be maybe specific exception?)
        if creator is None:
            raise CreatorError(
                "Creator {} was not found".format(identifier)
            )
        return creator

    def create(
        self,
        creator_identifier,
        variant,
        folder_entity=None,
        task_entity=None,
        pre_create_data=None,
        active=None
    ):
        """Trigger create of plugins with standartized arguments.

        Arguments 'folder_entity' and 'task_name' use current context as
        default values. If only 'task_entity' is provided it will be
        overridden by task name from current context. If 'task_name' is not
        provided when 'folder_entity' is, it is considered that task name is
        not specified, which can lead to error if product name template
        requires task name.

        Args:
            creator_identifier (str): Identifier of creator plugin.
            variant (str): Variant used for product name.
            folder_entity (Dict[str, Any]): Folder entity which define context
                of creation (possible context of created instance/s).
            task_entity (Dict[str, Any]): Task entity.
            pre_create_data (Dict[str, Any]): Pre-create attribute values.
            active (Optional[bool]): Whether the created instance defaults
                to be active or not.

        Returns:
            Any: Output of triggered creator's 'create' method.

        Raises:
            CreatorError: If creator was not found or folder is empty.

        """
        creator = self._get_creator_in_create(creator_identifier)

        project_name = self.project_name
        if folder_entity is None:
            folder_path = self.get_current_folder_path()
            folder_entity = ayon_api.get_folder_by_path(
                project_name, folder_path
            )
            if folder_entity is None:
                raise CreatorError(
                    "Folder '{}' was not found".format(folder_path)
                )

        if task_entity is None:
            current_task_name = self.get_current_task_name()
            if current_task_name:
                task_entity = ayon_api.get_task_by_name(
                    project_name, folder_entity["id"], current_task_name
                )

        if pre_create_data is None:
            pre_create_data = {}

        precreate_attr_defs = []
        # Hidden creators do not have or need the pre-create attributes.
        if isinstance(creator, Creator):
            precreate_attr_defs = creator.get_pre_create_attr_defs()

        # Create default values of precreate data
        _pre_create_data = get_default_values(precreate_attr_defs)
        # Update passed precreate data to default values
        # TODO validate types
        _pre_create_data.update(pre_create_data)

        project_entity = self.get_current_project_entity()
        args = (
            project_name,
            folder_entity,
            task_entity,
            variant,
            self.host_name,
        )
        kwargs = {"project_entity": project_entity}
        # Backwards compatibility for 'project_entity' argument
        # - 'get_product_name' signature changed 24/07/08
        if not is_func_signature_supported(
            creator.get_product_name, *args, **kwargs
        ):
            kwargs.pop("project_entity")
        product_name = creator.get_product_name(*args, **kwargs)

        instance_data = {
            "folderPath": folder_entity["path"],
            "task": task_entity["name"] if task_entity else None,
            "productType": creator.product_type,
            "variant": variant
        }
        if active is not None:
            if not isinstance(active, bool):
                self.log.warning(
                    "CreateContext.create 'active' argument is not a bool. "
                    f"Converting {active} {type(active)} to bool.")
                active = bool(active)
            instance_data["active"] = active

        with self.bulk_add_instances():
            return creator.create(
                product_name,
                instance_data,
                _pre_create_data
            )

    def create_with_unified_error(self, identifier, *args, **kwargs):
        """Trigger create but raise only one error if anything fails.

        Added to raise unified exception. Capture any possible issues and
        reraise it with unified information.

        Args:
            identifier (str): Identifier of creator.
            *args (Tuple[Any]): Arguments for create method.
            **kwargs (Dict[Any, Any]): Keyword argument for create method.

        Raises:
            CreatorsCreateFailed: When creation fails due to any possible
                reason. If anything goes wrong this is only possible exception
                the method should raise.

        """
        result, fail_info = self._create_with_unified_error(
            identifier, None, *args, **kwargs
        )
        if fail_info is not None:
            raise CreatorsCreateFailed([fail_info])
        return result

    def creator_removed_instance(self, instance: "CreatedInstance"):
        """When creator removes instance context should be acknowledged.

        If creator removes instance context should know about it to avoid
        possible issues in the session.

        Args:
            instance (CreatedInstance): Object of instance which was removed
                from scene metadata.
        """

        self._remove_instances([instance])

    def add_convertor_item(self, convertor_identifier, label):
        self.convertor_items_by_id[convertor_identifier] = ConvertorItem(
            convertor_identifier, label
        )

    def remove_convertor_item(self, convertor_identifier):
        self.convertor_items_by_id.pop(convertor_identifier, None)

    @contextmanager
    def bulk_add_instances(self, sender=None):
        with self._bulk_context("add", sender) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_instances_collection(self, sender=None):
        """DEPRECATED use 'bulk_add_instances' instead."""
        # TODO add warning
        with self.bulk_add_instances(sender) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_remove_instances(self, sender=None):
        with self._bulk_context("remove", sender) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_value_changes(self, sender=None):
        with self._bulk_context("change", sender) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_pre_create_attr_defs_change(self, sender=None):
        with self._bulk_context(
            "pre_create_attrs_change", sender
        ) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_create_attr_defs_change(self, sender=None):
        with self._bulk_context(
            "create_attrs_change", sender
        ) as bulk_info:
            yield bulk_info

    @contextmanager
    def bulk_publish_attr_defs_change(self, sender=None):
        with self._bulk_context("publish_attrs_change", sender) as bulk_info:
            yield bulk_info

    # --- instance change callbacks ---
    def create_plugin_pre_create_attr_defs_changed(self, identifier: str):
        """Create plugin pre-create attributes changed.

        Triggered by 'Creator'.

        Args:
            identifier (str): Create plugin identifier.

        """
        with self.bulk_pre_create_attr_defs_change() as bulk_item:
            bulk_item.append(identifier)

    def instance_create_attr_defs_changed(self, instance_id: str):
        """Instance attribute definitions changed.

        Triggered by instance 'CreatorAttributeValues' on instance.

        Args:
            instance_id (str): Instance id.

        """
        if self._is_instance_events_ready(instance_id):
            with self.bulk_create_attr_defs_change() as bulk_item:
                bulk_item.append(instance_id)

    def instance_publish_attr_defs_changed(
        self, instance_id: Optional[str], plugin_name: str
    ):
        """Instance attribute definitions changed.

        Triggered by instance 'PublishAttributeValues' on instance.

        Args:
            instance_id (Optional[str]): Instance id or None for context.
            plugin_name (str): Plugin name which attribute definitions were
                changed.

        """
        if self._is_instance_events_ready(instance_id):
            with self.bulk_publish_attr_defs_change() as bulk_item:
                bulk_item.append((instance_id, plugin_name))

    def instance_values_changed(
        self, instance_id: Optional[str], new_values: Dict[str, Any]
    ):
        """Instance value changed.

        Triggered by `CreatedInstance, 'CreatorAttributeValues'
            or 'PublishAttributeValues' on instance.

        Args:
            instance_id (Optional[str]): Instance id or None for context.
            new_values (Dict[str, Any]): Changed values.

        """
        if self._is_instance_events_ready(instance_id):
            with self.bulk_value_changes() as bulk_item:
                bulk_item.append((instance_id, new_values))

    # --- context change callbacks ---
    def publish_attribute_value_changed(
        self, plugin_name: str, value: Dict[str, Any]
    ):
        """Context publish attribute values changed.

        Triggered by instance 'PublishAttributeValues' on context.

        Args:
            plugin_name (str): Plugin name which changed value.
            value (Dict[str, Any]): Changed values.

        """
        self.instance_values_changed(
            None,
            {
                "publish_attributes": {
                    plugin_name: value,
                },
            },
        )

    def reset_instances(self):
        """Reload instances"""
        self._instances_by_id = collections.OrderedDict()

        # Collect instances
        error_message = "Collection of instances for creator {} failed. {}"
        failed_info = []
        for creator in self.sorted_creators:
            label = creator.label
            identifier = creator.identifier
            failed = False
            add_traceback = False
            exc_info = None
            try:
                creator.collect_instances()

            except CreatorError:
                failed = True
                exc_info = sys.exc_info()
                self.log.warning(error_message.format(identifier, exc_info[1]))

            except:  # noqa: E722
                failed = True
                add_traceback = True
                exc_info = sys.exc_info()
                self.log.warning(
                    error_message.format(identifier, ""),
                    exc_info=True
                )

            if failed:
                failed_info.append(
                    prepare_failed_creator_operation_info(
                        identifier, label, exc_info, add_traceback
                    )
                )

        if failed_info:
            raise CreatorsCollectionFailed(failed_info)

    def find_convertor_items(self):
        """Go through convertor plugins to look for items to convert.

        Raises:
            ConvertorsFindFailed: When one or more convertors fails during
                finding.
        """

        self.convertor_items_by_id = {}

        failed_info = []
        for convertor in self.convertors_plugins.values():
            try:
                convertor.find_instances()

            except:  # noqa: E722
                failed_info.append(
                    prepare_failed_convertor_operation_info(
                        convertor.identifier, sys.exc_info()
                    )
                )
                self.log.warning(
                    "Failed to find instances of convertor \"{}\"".format(
                        convertor.identifier
                    ),
                    exc_info=True
                )

        if failed_info:
            raise ConvertorsFindFailed(failed_info)

    def execute_autocreators(self):
        """Execute discovered AutoCreator plugins.

        Reset instances if any autocreator executed properly.
        """

        failed_info = []
        for creator in self.sorted_autocreators:
            identifier = creator.identifier
            _, fail_info = self._create_with_unified_error(identifier, creator)
            if fail_info is not None:
                failed_info.append(fail_info)

        if failed_info:
            raise CreatorsCreateFailed(failed_info)

    def get_folder_entities(self, folder_paths: Iterable[str]):
        """Get folder entities by paths.

        Args:
            folder_paths (Iterable[str]): Folder paths.

        Returns:
            Dict[str, Optional[Dict[str, Any]]]: Folder entities by path.

        """
        output = {
            folder_path: None
            for folder_path in folder_paths
        }
        remainder_paths = set()
        for folder_path in output:
            # Skip invalid folder paths (folder name or empty path)
            if not folder_path or "/" not in folder_path:
                continue

            if folder_path not in self._folder_entities_by_path:
                remainder_paths.add(folder_path)
                continue

            output[folder_path] = self._folder_entities_by_path[folder_path]

        if not remainder_paths:
            return output

        found_paths = set()
        for folder_entity in ayon_api.get_folders(
            self.project_name,
            folder_paths=remainder_paths,
        ):
            folder_path = folder_entity["path"]
            found_paths.add(folder_path)
            output[folder_path] = folder_entity
            self._folder_entities_by_path[folder_path] = folder_entity

        # Cache empty folder entities
        for path in remainder_paths - found_paths:
            self._folder_entities_by_path[path] = None

        return output

    def get_task_entities(
        self,
        task_names_by_folder_paths: Dict[str, Set[str]]
    ) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]:
        """Get task entities by folder path and task name.

        Entities are cached until reset.

        Args:
            task_names_by_folder_paths (Dict[str, Set[str]]): Task names by
                folder path.

        Returns:
            Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path
                and task name.

        """
        output = {}
        for folder_path, task_names in task_names_by_folder_paths.items():
            if folder_path is None:
                continue
            output[folder_path] = {
                task_name: None
                for task_name in task_names
                if task_name is not None
            }

        missing_folder_paths = set()
        for folder_path, output_task_entities_by_name in output.items():
            if not output_task_entities_by_name:
                continue

            if folder_path not in self._task_ids_by_folder_path:
                missing_folder_paths.add(folder_path)
                continue

            all_tasks_filled = True
            task_ids = self._task_ids_by_folder_path[folder_path]
            task_entities_by_name = {}
            for task_id in task_ids:
                task_entity = self._task_entities_by_id.get(task_id)
                if task_entity is None:
                    all_tasks_filled = False
                    continue
                task_entities_by_name[task_entity["name"]] = task_entity

            any_missing = False
            for task_name in set(output_task_entities_by_name):
                task_entity = task_entities_by_name.get(task_name)
                if task_entity is None:
                    any_missing = True
                    continue

                output_task_entities_by_name[task_name] = task_entity

            if any_missing and not all_tasks_filled:
                missing_folder_paths.add(folder_path)

        if not missing_folder_paths:
            return output

        folder_entities_by_path = self.get_folder_entities(
            missing_folder_paths
        )
        folder_path_by_id = {}
        for folder_path, folder_entity in folder_entities_by_path.items():
            if folder_entity is not None:
                folder_path_by_id[folder_entity["id"]] = folder_path

        if not folder_path_by_id:
            return output

        task_entities_by_parent_id = collections.defaultdict(list)
        for task_entity in ayon_api.get_tasks(
            self.project_name,
            folder_ids=folder_path_by_id.keys()
        ):
            folder_id = task_entity["folderId"]
            task_entities_by_parent_id[folder_id].append(task_entity)

        for folder_id, task_entities in task_entities_by_parent_id.items():
            folder_path = folder_path_by_id[folder_id]
            task_ids = set()
            task_names = set()
            for task_entity in task_entities:
                task_id = task_entity["id"]
                task_name = task_entity["name"]
                task_ids.add(task_id)
                task_names.add(task_name)
                self._task_entities_by_id[task_id] = task_entity

                output[folder_path][task_name] = task_entity
            self._task_ids_by_folder_path[folder_path] = task_ids
            self._task_names_by_folder_path[folder_path] = task_names

        return output

    def get_folder_entity(
        self,
        folder_path: Optional[str],
    ) -> Optional[Dict[str, Any]]:
        """Get folder entity by path.

        Entities are cached until reset.

        Args:
            folder_path (Optional[str]): Folder path.

        Returns:
            Optional[Dict[str, Any]]: Folder entity.

        """
        if not folder_path:
            return None
        return self.get_folder_entities([folder_path]).get(folder_path)

    def get_task_entity(
        self,
        folder_path: Optional[str],
        task_name: Optional[str],
    ) -> Optional[Dict[str, Any]]:
        """Get task entity by name and folder path.

        Entities are cached until reset.

        Args:
            folder_path (Optional[str]): Folder path.
            task_name (Optional[str]): Task name.

        Returns:
            Optional[Dict[str, Any]]: Task entity.

        """
        if not folder_path or not task_name:
            return None

        output = self.get_task_entities({folder_path: {task_name}})
        return output.get(folder_path, {}).get(task_name)

    def get_instances_folder_entities(
        self, instances: Optional[Iterable["CreatedInstance"]] = None
    ) -> Dict[str, Optional[Dict[str, Any]]]:
        if instances is None:
            instances = self._instances_by_id.values()
        instances = list(instances)
        output = {
            instance.id: None
            for instance in instances
        }
        if not instances:
            return output

        folder_paths = {
            instance.get("folderPath")
            for instance in instances
        }
        folder_paths.discard(None)
        folder_entities_by_path = self.get_folder_entities(folder_paths)
        for instance in instances:
            folder_path = instance.get("folderPath")
            output[instance.id] = folder_entities_by_path.get(folder_path)
        return output

    def get_instances_task_entities(
        self, instances: Optional[Iterable["CreatedInstance"]] = None
    ):
        """Get task entities for instances.

        Args:
            instances (Optional[Iterable[CreatedInstance]]): Instances to
                get task entities. If not provided all instances are used.

        Returns:
            Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id.

        """
        if instances is None:
            instances = self._instances_by_id.values()
        instances = list(instances)

        output = {
            instance.id: None
            for instance in instances
        }
        if not instances:
            return output

        filtered_instances = []
        task_names_by_folder_path = collections.defaultdict(set)
        for instance in instances:
            folder_path = instance.get("folderPath")
            task_name = instance.get("task")
            if not folder_path or not task_name:
                continue
            filtered_instances.append(instance)
            task_names_by_folder_path[folder_path].add(task_name)

        task_entities_by_folder_path = self.get_task_entities(
            task_names_by_folder_path
        )
        for instance in filtered_instances:
            folder_path = instance["folderPath"]
            task_name = instance["task"]
            output[instance.id] = (
                task_entities_by_folder_path[folder_path][task_name]
            )

        return output

    def get_instances_context_info(
        self, instances: Optional[Iterable["CreatedInstance"]] = None
    ) -> Dict[str, InstanceContextInfo]:
        """Validate 'folder' and 'task' instance context.

        Args:
            instances (Optional[Iterable[CreatedInstance]]): Instances to
                validate. If not provided all instances are validated.

        Returns:
            Dict[str, InstanceContextInfo]: Validation results by instance id.

        """
        # Use all instances from context if 'instances' are not passed
        if instances is None:
            instances = self._instances_by_id.values()
        instances = tuple(instances)
        info_by_instance_id = {
            instance.id: InstanceContextInfo(
                instance.get("folderPath"),
                instance.get("task"),
                False,
                False,
            )
            for instance in instances
        }

        # Skip if instances are empty
        if not info_by_instance_id:
            return info_by_instance_id

        project_name = self.project_name

        to_validate = []
        task_names_by_folder_path = collections.defaultdict(set)
        for instance in instances:
            context_info = info_by_instance_id[instance.id]
            if instance.has_promised_context:
                context_info.folder_is_valid = True
                context_info.task_is_valid = True
                # NOTE missing task type
                continue
            # TODO allow context promise
            folder_path = context_info.folder_path
            if not folder_path:
                continue

            if folder_path in self._folder_entities_by_path:
                folder_entity = self._folder_entities_by_path[folder_path]
                if folder_entity is None:
                    continue
                context_info.folder_is_valid = True

            task_name = context_info.task_name
            if task_name is not None:
                tasks_cache = self._task_names_by_folder_path.get(folder_path)
                if tasks_cache is not None:
                    context_info.task_is_valid = task_name in tasks_cache
                    continue

            to_validate.append(instance)
            task_names_by_folder_path[folder_path].add(task_name)

        if not to_validate:
            return info_by_instance_id

        # Backwards compatibility for cases where folder name is set instead
        #   of folder path
        folder_paths = set()
        task_names_by_folder_name = {}
        task_names_by_folder_path_clean = {}
        for folder_path, task_names in task_names_by_folder_path.items():
            if folder_path is None:
                continue

            clean_task_names = {
                task_name
                for task_name in task_names
                if task_name
            }

            if "/" not in folder_path:
                task_names_by_folder_name[folder_path] = clean_task_names
                continue

            folder_paths.add(folder_path)
            if not clean_task_names:
                continue

            task_names_by_folder_path_clean[folder_path] = clean_task_names

        folder_paths_by_name = collections.defaultdict(list)
        if task_names_by_folder_name:
            for folder_entity in ayon_api.get_folders(
                project_name,
                folder_names=task_names_by_folder_name.keys(),
                fields={"name", "path"}
            ):
                folder_name = folder_entity["name"]
                folder_path = folder_entity["path"]
                folder_paths_by_name[folder_name].append(folder_path)

        folder_path_by_name = {}
        for folder_name, paths in folder_paths_by_name.items():
            if len(paths) != 1:
                continue
            path = paths[0]
            folder_path_by_name[folder_name] = path
            folder_paths.add(path)
            clean_task_names = task_names_by_folder_name[folder_name]
            if not clean_task_names:
                continue
            folder_task_names = task_names_by_folder_path_clean.setdefault(
                path, set()
            )
            folder_task_names |= clean_task_names

        folder_entities_by_path = self.get_folder_entities(folder_paths)
        task_entities_by_folder_path = self.get_task_entities(
            task_names_by_folder_path_clean
        )

        for instance in to_validate:
            folder_path = instance["folderPath"]
            task_name = instance.get("task")
            if folder_path and "/" not in folder_path:
                new_folder_path = folder_path_by_name.get(folder_path)
                if new_folder_path:
                    folder_path = new_folder_path
                    instance["folderPath"] = new_folder_path

            folder_entity = folder_entities_by_path.get(folder_path)
            if not folder_entity:
                continue
            context_info = info_by_instance_id[instance.id]
            context_info.folder_is_valid = True

            if (
                not task_name
                or task_name in task_entities_by_folder_path[folder_path]
            ):
                context_info.task_is_valid = True
        return info_by_instance_id

    def save_changes(self):
        """Save changes. Update all changed values."""
        if not self.host_is_valid:
            missing_methods = self.get_host_misssing_methods(self.host)
            raise HostMissRequiredMethod(self.host, missing_methods)

        self._save_context_changes()
        self._save_instance_changes()

    def _save_context_changes(self):
        """Save global context values."""
        changes = self.context_data_changes()
        if changes:
            data = self.context_data_to_store()
            self.host.update_context_data(data, changes)

    def _save_instance_changes(self):
        """Save instance specific values."""
        instances_by_identifier = collections.defaultdict(list)
        for instance in self._instances_by_id.values():
            instance_changes = instance.changes()
            if not instance_changes:
                continue

            identifier = instance.creator_identifier
            instances_by_identifier[identifier].append(
                UpdateData(instance, instance_changes)
            )

        if not instances_by_identifier:
            return

        error_message = "Instances update of creator \"{}\" failed. {}"
        failed_info = []

        for creator in self.get_sorted_creators(
            instances_by_identifier.keys()
        ):
            identifier = creator.identifier
            update_list = instances_by_identifier[identifier]
            if not update_list:
                continue

            label = creator.label
            failed = False
            add_traceback = False
            exc_info = None
            try:
                creator.update_instances(update_list)

            except CreatorError:
                failed = True
                exc_info = sys.exc_info()
                self.log.warning(error_message.format(identifier, exc_info[1]))

            except:  # noqa: E722
                failed = True
                add_traceback = True
                exc_info = sys.exc_info()
                self.log.warning(
                    error_message.format(identifier, ""), exc_info=True)

            if failed:
                failed_info.append(
                    prepare_failed_creator_operation_info(
                        identifier, label, exc_info, add_traceback
                    )
                )
            else:
                for update_data in update_list:
                    instance = update_data.instance
                    instance.mark_as_stored()

        if failed_info:
            raise CreatorsSaveFailed(failed_info)

    def remove_instances(self, instances, sender=None):
        """Remove instances from context.

        All instances that don't have creator identifier leading to existing
            creator are just removed from context.

        Args:
            instances (List[CreatedInstance]): Instances that should be
                removed. Remove logic is done using creator, which may require
                to do other cleanup than just remove instance from context.
            sender (Optional[str]): Sender of the event.

        """
        instances_by_identifier = collections.defaultdict(list)
        for instance in instances:
            identifier = instance.creator_identifier
            instances_by_identifier[identifier].append(instance)

        # Just remove instances from context if creator is not available
        missing_creators = set(instances_by_identifier) - set(self.creators)
        instances = []
        for identifier in missing_creators:
            instances.extend(
                instance
                for instance in instances_by_identifier[identifier]
            )

        self._remove_instances(instances, sender)

        error_message = "Instances removement of creator \"{}\" failed. {}"
        failed_info = []
        # Remove instances by creator plugin order
        for creator in self.get_sorted_creators(
            instances_by_identifier.keys()
        ):
            identifier = creator.identifier
            creator_instances = instances_by_identifier[identifier]

            label = creator.label
            failed = False
            add_traceback = False
            exc_info = None
            try:
                creator.remove_instances(creator_instances)

            except CreatorError:
                failed = True
                exc_info = sys.exc_info()
                self.log.warning(
                    error_message.format(identifier, exc_info[1])
                )

            except (KeyboardInterrupt, SystemExit):
                raise

            except:  # noqa: E722
                failed = True
                add_traceback = True
                exc_info = sys.exc_info()
                self.log.warning(
                    error_message.format(identifier, ""),
                    exc_info=True
                )

            if failed:
                failed_info.append(
                    prepare_failed_creator_operation_info(
                        identifier, label, exc_info, add_traceback
                    )
                )

        if failed_info:
            raise CreatorsRemoveFailed(failed_info)

    @property
    def collection_shared_data(self):
        """Access to shared data that can be used during creator's collection.

        Returns:
            Dict[str, Any]: Shared data.

        Raises:
            UnavailableSharedData: When called out of collection phase.
        """

        if self._collection_shared_data is None:
            raise UnavailableSharedData(
                "Accessed Collection shared data out of collection phase"
            )
        return self._collection_shared_data

    def run_convertor(self, convertor_identifier):
        """Run convertor plugin by identifier.

        Conversion is skipped if convertor is not available.

        Args:
            convertor_identifier (str): Identifier of convertor.
        """

        convertor = self.convertors_plugins.get(convertor_identifier)
        if convertor is not None:
            convertor.convert()

    def run_convertors(self, convertor_identifiers):
        """Run convertor plugins by identifiers.

        Conversion is skipped if convertor is not available. It is recommended
        to trigger reset after conversion to reload instances.

        Args:
            convertor_identifiers (Iterator[str]): Identifiers of convertors
                to run.

        Raises:
            ConvertorsConversionFailed: When one or more convertors fails.
        """

        failed_info = []
        for convertor_identifier in convertor_identifiers:
            try:
                self.run_convertor(convertor_identifier)

            except:  # noqa: E722
                failed_info.append(
                    prepare_failed_convertor_operation_info(
                        convertor_identifier, sys.exc_info()
                    )
                )
                self.log.warning(
                    "Failed to convert instances of convertor \"{}\"".format(
                        convertor_identifier
                    ),
                    exc_info=True
                )

        if failed_info:
            raise ConvertorsConversionFailed(failed_info)

    def _register_event_callback(self, topic: str, callback: Callable):
        return self._event_hub.add_callback(topic, callback)

    def _emit_event(
        self,
        topic: str,
        data: Optional[Dict[str, Any]] = None,
        sender: Optional[str] = None,
    ):
        if data is None:
            data = {}
        data.setdefault("create_context", self)
        return self._event_hub.emit(topic, data, sender)

    def _remove_instances(self, instances, sender=None):
        with self.bulk_remove_instances(sender) as bulk_info:
            for instance in instances:
                obj = self._instances_by_id.pop(instance.id, None)
                if obj is not None:
                    bulk_info.append(obj)

    def _create_with_unified_error(
        self, identifier, creator, *args, **kwargs
    ):
        error_message = "Failed to run Creator with identifier \"{}\". {}"

        label = None
        add_traceback = False
        result = None
        fail_info = None
        exc_info = None
        success = False

        try:
            # Try to get creator and his label
            if creator is None:
                creator = self._get_creator_in_create(identifier)
            label = getattr(creator, "label", label)

            # Run create
            with self.bulk_add_instances():
                result = creator.create(*args, **kwargs)
            success = True

        except CreatorError:
            exc_info = sys.exc_info()
            self.log.warning(error_message.format(identifier, exc_info[1]))

        except:  # noqa: E722
            add_traceback = True
            exc_info = sys.exc_info()
            self.log.warning(
                error_message.format(identifier, ""),
                exc_info=True
            )

        if not success:
            fail_info = prepare_failed_creator_operation_info(
                identifier, label, exc_info, add_traceback
            )
        return result, fail_info

    def _is_instance_events_ready(self, instance_id: Optional[str]) -> bool:
        # Context is ready
        if instance_id is None:
            return True
        # Instance is not in yet in context
        if instance_id not in self._instances_by_id:
            return False

        # Instance in 'collect' bulk will be ignored
        for instance in self._bulk_info["add"].get_data():
            if instance.id == instance_id:
                return False
        return True

    @contextmanager
    def _bulk_context(self, key: str, sender: Optional[str]):
        bulk_info = self._bulk_info[key]
        bulk_info.set_sender(sender)

        bulk_info.increase()
        if key not in self._bulk_order:
            self._bulk_order.append(key)
        try:
            yield bulk_info
        finally:
            bulk_info.decrease()
            if bulk_info:
                self._bulk_finished(key)

    def _bulk_finished(self, key: str):
        if self._bulk_order[0] != key:
            return

        self._bulk_order.pop(0)
        self._bulk_finish(key)

        while self._bulk_order:
            key = self._bulk_order[0]
            if not self._bulk_info[key]:
                break
            self._bulk_order.pop(0)
            self._bulk_finish(key)

    def _bulk_finish(self, key: str):
        bulk_info = self._bulk_info[key]
        sender = bulk_info.get_sender()
        data = bulk_info.pop_data()
        if key == "add":
            self._bulk_add_instances_finished(data, sender)
        elif key == "remove":
            self._bulk_remove_instances_finished(data, sender)
        elif key == "change":
            self._bulk_values_change_finished(data, sender)
        elif key == "pre_create_attrs_change":
            self._bulk_pre_create_attrs_change_finished(data, sender)
        elif key == "create_attrs_change":
            self._bulk_create_attrs_change_finished(data, sender)
        elif key == "publish_attrs_change":
            self._bulk_publish_attrs_change_finished(data, sender)

    def _bulk_add_instances_finished(
        self,
        instances_to_validate: List["CreatedInstance"],
        sender: Optional[str]
    ):
        if not instances_to_validate:
            return

        # Set publish attributes before bulk callbacks are triggered
        for instance in instances_to_validate:
            publish_attributes = instance.publish_attributes
            # Prepare publish plugin attributes and set it on instance
            for plugin in self.plugins_with_defs:
                try:
                    if is_func_signature_supported(
                            plugin.convert_attribute_values, self, instance
                    ):
                        plugin.convert_attribute_values(self, instance)

                    elif plugin.__instanceEnabled__:
                        output = plugin.convert_attribute_values(
                            publish_attributes
                        )
                        if output:
                            publish_attributes.update(output)

                except Exception:
                    self.log.error(
                        "Failed to convert attribute values of"
                        f" plugin '{plugin.__name__}'",
                        exc_info=True
                    )

            for plugin in self.plugins_with_defs:
                attr_defs = None
                try:
                    attr_defs = plugin.get_attr_defs_for_instance(
                        self, instance
                    )
                except Exception:
                    self.log.error(
                        "Failed to get attribute definitions"
                        f" from plugin '{plugin.__name__}'.",
                        exc_info=True
                    )

                if not attr_defs:
                    continue
                instance.set_publish_plugin_attr_defs(
                    plugin.__name__, attr_defs
                )

        # Cache folder and task entities for all instances at once
        self.get_instances_context_info(instances_to_validate)

        self._emit_event(
            INSTANCE_ADDED_TOPIC,
            {
                "instances": instances_to_validate,
            },
            sender,
        )

    def _bulk_remove_instances_finished(
        self,
        instances_to_remove: List["CreatedInstance"],
        sender: Optional[str]
    ):
        if not instances_to_remove:
            return

        self._emit_event(
            INSTANCE_REMOVED_TOPIC,
            {
                "instances": instances_to_remove,
            },
            sender,
        )

    def _bulk_values_change_finished(
        self,
        changes: Tuple[Union[str, None], Dict[str, Any]],
        sender: Optional[str],
    ):
        if not changes:
            return
        item_data_by_id = {}
        for item_id, item_changes in changes:
            item_values = item_data_by_id.setdefault(item_id, {})
            if "creator_attributes" in item_changes:
                current_value = item_values.setdefault(
                    "creator_attributes", {}
                )
                current_value.update(
                    item_changes.pop("creator_attributes")
                )

            if "publish_attributes" in item_changes:
                current_publish = item_values.setdefault(
                    "publish_attributes", {}
                )
                for plugin_name, plugin_value in item_changes.pop(
                    "publish_attributes"
                ).items():
                    plugin_changes = current_publish.setdefault(
                        plugin_name, {}
                    )
                    plugin_changes.update(plugin_value)

            item_values.update(item_changes)

        event_changes = []
        for item_id, item_changes in item_data_by_id.items():
            instance = self.get_instance_by_id(item_id)
            event_changes.append({
                "instance": instance,
                "changes": item_changes,
            })

        event_data = {
            "changes": event_changes,
        }

        self._emit_event(
            VALUE_CHANGED_TOPIC,
            event_data,
            sender
        )

    def _bulk_pre_create_attrs_change_finished(
        self, identifiers: List[str], sender: Optional[str]
    ):
        if not identifiers:
            return
        identifiers = list(set(identifiers))
        self._emit_event(
            PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC,
            {
                "identifiers": identifiers,
            },
            sender,
        )

    def _bulk_create_attrs_change_finished(
        self, instance_ids: List[str], sender: Optional[str]
    ):
        if not instance_ids:
            return

        instances = [
            self.get_instance_by_id(instance_id)
            for instance_id in set(instance_ids)
        ]
        self._emit_event(
            CREATE_ATTR_DEFS_CHANGED_TOPIC,
            {
                "instances": instances,
            },
            sender,
        )

    def _bulk_publish_attrs_change_finished(
        self,
        attr_info: Tuple[str, Union[str, None]],
        sender: Optional[str],
    ):
        if not attr_info:
            return

        instance_changes = {}
        for instance_id, plugin_name in attr_info:
            instance_data = instance_changes.setdefault(
                instance_id,
                {
                    "instance": None,
                    "plugin_names": set(),
                }
            )
            instance = self.get_instance_by_id(instance_id)
            instance_data["instance"] = instance
            instance_data["plugin_names"].add(plugin_name)

        self._emit_event(
            PUBLISH_ATTR_DEFS_CHANGED_TOPIC,
            {"instance_changes": instance_changes},
            sender,
        )

collection_shared_data property

Access to shared data that can be used during creator's collection.

Returns:

Type Description

Dict[str, Any]: Shared data.

Raises:

Type Description
UnavailableSharedData

When called out of collection phase.

context_has_changed property

Host context has changed.

As context is used project, folder, task name and workfile path if host does support workfiles.

Returns:

Name Type Description
bool

Context changed.

host_is_valid property

Is host valid for creation.

log property

Dynamic access to logger.

publish_attributes property

Access to global publish attributes.

sorted_autocreators property

Sorted auto-creators by 'order' attribute.

Returns:

Type Description

List[AutoCreator]: Sorted plugins by 'order' value.

sorted_creators property

Sorted creators by 'order' attribute.

Returns:

Type Description

List[BaseCreator]: Sorted creator plugins by 'order' value.

add_create_attr_defs_change_callback(callback)

Register callback to listen create attribute changes.

Create plugin changed attribute definitions of instance.

Data structure of event::

```python
{
    "instances": [CreatedInstance, ...],
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when create attributes changed.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
def add_create_attr_defs_change_callback (self, callback):
    """Register callback to listen create attribute changes.

    Create plugin changed attribute definitions of instance.

    Data structure of event::

        ```python
        {
            "instances": [CreatedInstance, ...],
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            create attributes changed.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback)

add_instances_added_callback(callback)

Register callback for added instances.

Event is triggered when instances are already available in context and have set create/publish attribute definitions.

Data structure of event::

```python
{
    "instances": [CreatedInstance, ...],
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when instances are added to context.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
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
def add_instances_added_callback(self, callback):
    """Register callback for added instances.

    Event is triggered when instances are already available in context
        and have set create/publish attribute definitions.

    Data structure of event::

        ```python
        {
            "instances": [CreatedInstance, ...],
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            instances are added to context.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    return self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback)

add_instances_removed_callback(callback)

Register callback for removed instances.

Event is triggered when instances are already removed from context.

Data structure of event::

```python
{
    "instances": [CreatedInstance, ...],
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when instances are removed from context.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
def add_instances_removed_callback (self, callback):
    """Register callback for removed instances.

    Event is triggered when instances are already removed from context.

    Data structure of event::

        ```python
        {
            "instances": [CreatedInstance, ...],
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            instances are removed from context.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback)

add_pre_create_attr_defs_change_callback(callback)

Register callback to listen pre-create attribute changes.

Create plugin can trigger refresh of pre-create attributes. Usage of this event is mainly for publisher UI.

Data structure of event::

```python
{
    "identifiers": ["create_plugin_identifier"],
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when pre-create attributes should be refreshed.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
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
def add_pre_create_attr_defs_change_callback (self, callback):
    """Register callback to listen pre-create attribute changes.

    Create plugin can trigger refresh of pre-create attributes. Usage of
        this event is mainly for publisher UI.

    Data structure of event::

        ```python
        {
            "identifiers": ["create_plugin_identifier"],
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            pre-create attributes should be refreshed.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    self._event_hub.add_callback(
        PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback
    )

add_publish_attr_defs_change_callback(callback)

Register callback to listen publish attribute changes.

Publish plugin changed attribute definitions of instance of context.

Data structure of event::

```python
{
    "instance_changes": {
        None: {
            "instance": None,
            "plugin_names": {"PluginA"},
        }
        "<instance_id>": {
            "instance": CreatedInstance,
            "plugin_names": {"PluginB", "PluginC"},
        }
    },
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when publish attributes changed.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
def add_publish_attr_defs_change_callback (self, callback):
    """Register callback to listen publish attribute changes.

    Publish plugin changed attribute definitions of instance of context.

    Data structure of event::

        ```python
        {
            "instance_changes": {
                None: {
                    "instance": None,
                    "plugin_names": {"PluginA"},
                }
                "<instance_id>": {
                    "instance": CreatedInstance,
                    "plugin_names": {"PluginB", "PluginC"},
                }
            },
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            publish attributes changed.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    self._event_hub.add_callback(
        PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback
    )

add_value_changed_callback(callback)

Register callback to listen value changes.

Event is triggered when any value changes on any instance or context data.

Data structure of event::

```python
{
    "changes": [
        {
            "instance": CreatedInstance,
            "changes": {
                "folderPath": "/new/folder/path",
                "creator_attributes": {
                    "attr_1": "value_1"
                }
            }
        }
    ],
    "create_context": CreateContext
}
```

Parameters:

Name Type Description Default
callback Callable

Callback function that will be called when value changed.

required

Returns:

Name Type Description
EventCallback

Created callback object which can be used to stop listening.

Source code in client/ayon_core/pipeline/create/context.py
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
def add_value_changed_callback(self, callback):
    """Register callback to listen value changes.

    Event is triggered when any value changes on any instance or
        context data.

    Data structure of event::

        ```python
        {
            "changes": [
                {
                    "instance": CreatedInstance,
                    "changes": {
                        "folderPath": "/new/folder/path",
                        "creator_attributes": {
                            "attr_1": "value_1"
                        }
                    }
                }
            ],
            "create_context": CreateContext
        }
        ```

    Args:
        callback (Callable): Callback function that will be called when
            value changed.

    Returns:
        EventCallback: Created callback object which can be used to
            stop listening.

    """
    self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback)

bulk_instances_collection(sender=None)

DEPRECATED use 'bulk_add_instances' instead.

Source code in client/ayon_core/pipeline/create/context.py
1262
1263
1264
1265
1266
1267
@contextmanager
def bulk_instances_collection(self, sender=None):
    """DEPRECATED use 'bulk_add_instances' instead."""
    # TODO add warning
    with self.bulk_add_instances(sender) as bulk_info:
        yield bulk_info

context_data_changes()

Changes of attributes.

Source code in client/ayon_core/pipeline/create/context.py
1034
1035
1036
1037
1038
1039
def context_data_changes(self):
    """Changes of attributes."""

    return TrackChangesItem(
        self._original_context_data, self.context_data_to_store()
    )

context_data_to_store()

Data that should be stored by host function.

The same data should be returned on loading.

Source code in client/ayon_core/pipeline/create/context.py
1025
1026
1027
1028
1029
1030
1031
1032
def context_data_to_store(self):
    """Data that should be stored by host function.

    The same data should be returned on loading.
    """
    return {
        "publish_attributes": self._publish_attributes.data_to_store()
    }

create(creator_identifier, variant, folder_entity=None, task_entity=None, pre_create_data=None, active=None)

Trigger create of plugins with standartized arguments.

Arguments 'folder_entity' and 'task_name' use current context as default values. If only 'task_entity' is provided it will be overridden by task name from current context. If 'task_name' is not provided when 'folder_entity' is, it is considered that task name is not specified, which can lead to error if product name template requires task name.

Parameters:

Name Type Description Default
creator_identifier str

Identifier of creator plugin.

required
variant str

Variant used for product name.

required
folder_entity Dict[str, Any]

Folder entity which define context of creation (possible context of created instance/s).

None
task_entity Dict[str, Any]

Task entity.

None
pre_create_data Dict[str, Any]

Pre-create attribute values.

None
active Optional[bool]

Whether the created instance defaults to be active or not.

None

Returns:

Name Type Description
Any

Output of triggered creator's 'create' method.

Raises:

Type Description
CreatorError

If creator was not found or folder is empty.

Source code in client/ayon_core/pipeline/create/context.py
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
def create(
    self,
    creator_identifier,
    variant,
    folder_entity=None,
    task_entity=None,
    pre_create_data=None,
    active=None
):
    """Trigger create of plugins with standartized arguments.

    Arguments 'folder_entity' and 'task_name' use current context as
    default values. If only 'task_entity' is provided it will be
    overridden by task name from current context. If 'task_name' is not
    provided when 'folder_entity' is, it is considered that task name is
    not specified, which can lead to error if product name template
    requires task name.

    Args:
        creator_identifier (str): Identifier of creator plugin.
        variant (str): Variant used for product name.
        folder_entity (Dict[str, Any]): Folder entity which define context
            of creation (possible context of created instance/s).
        task_entity (Dict[str, Any]): Task entity.
        pre_create_data (Dict[str, Any]): Pre-create attribute values.
        active (Optional[bool]): Whether the created instance defaults
            to be active or not.

    Returns:
        Any: Output of triggered creator's 'create' method.

    Raises:
        CreatorError: If creator was not found or folder is empty.

    """
    creator = self._get_creator_in_create(creator_identifier)

    project_name = self.project_name
    if folder_entity is None:
        folder_path = self.get_current_folder_path()
        folder_entity = ayon_api.get_folder_by_path(
            project_name, folder_path
        )
        if folder_entity is None:
            raise CreatorError(
                "Folder '{}' was not found".format(folder_path)
            )

    if task_entity is None:
        current_task_name = self.get_current_task_name()
        if current_task_name:
            task_entity = ayon_api.get_task_by_name(
                project_name, folder_entity["id"], current_task_name
            )

    if pre_create_data is None:
        pre_create_data = {}

    precreate_attr_defs = []
    # Hidden creators do not have or need the pre-create attributes.
    if isinstance(creator, Creator):
        precreate_attr_defs = creator.get_pre_create_attr_defs()

    # Create default values of precreate data
    _pre_create_data = get_default_values(precreate_attr_defs)
    # Update passed precreate data to default values
    # TODO validate types
    _pre_create_data.update(pre_create_data)

    project_entity = self.get_current_project_entity()
    args = (
        project_name,
        folder_entity,
        task_entity,
        variant,
        self.host_name,
    )
    kwargs = {"project_entity": project_entity}
    # Backwards compatibility for 'project_entity' argument
    # - 'get_product_name' signature changed 24/07/08
    if not is_func_signature_supported(
        creator.get_product_name, *args, **kwargs
    ):
        kwargs.pop("project_entity")
    product_name = creator.get_product_name(*args, **kwargs)

    instance_data = {
        "folderPath": folder_entity["path"],
        "task": task_entity["name"] if task_entity else None,
        "productType": creator.product_type,
        "variant": variant
    }
    if active is not None:
        if not isinstance(active, bool):
            self.log.warning(
                "CreateContext.create 'active' argument is not a bool. "
                f"Converting {active} {type(active)} to bool.")
            active = bool(active)
        instance_data["active"] = active

    with self.bulk_add_instances():
        return creator.create(
            product_name,
            instance_data,
            _pre_create_data
        )

create_plugin_pre_create_attr_defs_changed(identifier)

Create plugin pre-create attributes changed.

Triggered by 'Creator'.

Parameters:

Name Type Description Default
identifier str

Create plugin identifier.

required
Source code in client/ayon_core/pipeline/create/context.py
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
def create_plugin_pre_create_attr_defs_changed(self, identifier: str):
    """Create plugin pre-create attributes changed.

    Triggered by 'Creator'.

    Args:
        identifier (str): Create plugin identifier.

    """
    with self.bulk_pre_create_attr_defs_change() as bulk_item:
        bulk_item.append(identifier)

create_with_unified_error(identifier, *args, **kwargs)

Trigger create but raise only one error if anything fails.

Added to raise unified exception. Capture any possible issues and reraise it with unified information.

Parameters:

Name Type Description Default
identifier str

Identifier of creator.

required
*args Tuple[Any]

Arguments for create method.

()
**kwargs Dict[Any, Any]

Keyword argument for create method.

{}

Raises:

Type Description
CreatorsCreateFailed

When creation fails due to any possible reason. If anything goes wrong this is only possible exception the method should raise.

Source code in client/ayon_core/pipeline/create/context.py
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
def create_with_unified_error(self, identifier, *args, **kwargs):
    """Trigger create but raise only one error if anything fails.

    Added to raise unified exception. Capture any possible issues and
    reraise it with unified information.

    Args:
        identifier (str): Identifier of creator.
        *args (Tuple[Any]): Arguments for create method.
        **kwargs (Dict[Any, Any]): Keyword argument for create method.

    Raises:
        CreatorsCreateFailed: When creation fails due to any possible
            reason. If anything goes wrong this is only possible exception
            the method should raise.

    """
    result, fail_info = self._create_with_unified_error(
        identifier, None, *args, **kwargs
    )
    if fail_info is not None:
        raise CreatorsCreateFailed([fail_info])
    return result

creator_adds_instance(instance)

Creator adds new instance to context.

Instances should be added only from creators.

Parameters:

Name Type Description Default
instance(CreatedInstance)

Instance with prepared data from creator.

required

TODO: Rename method to more suit.

Source code in client/ayon_core/pipeline/create/context.py
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
def creator_adds_instance(self, instance: "CreatedInstance"):
    """Creator adds new instance to context.

    Instances should be added only from creators.

    Args:
        instance(CreatedInstance): Instance with prepared data from
            creator.

    TODO: Rename method to more suit.
    """
    # Add instance to instances list
    if instance.id in self._instances_by_id:
        self.log.warning((
            "Instance with id {} is already added to context."
        ).format(instance.id))
        return

    self._instances_by_id[instance.id] = instance

    # Add instance to be validated inside 'bulk_add_instances'
    #   context manager if is inside bulk
    with self.bulk_add_instances() as bulk_info:
        bulk_info.append(instance)

creator_removed_instance(instance)

When creator removes instance context should be acknowledged.

If creator removes instance context should know about it to avoid possible issues in the session.

Parameters:

Name Type Description Default
instance CreatedInstance

Object of instance which was removed from scene metadata.

required
Source code in client/ayon_core/pipeline/create/context.py
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
def creator_removed_instance(self, instance: "CreatedInstance"):
    """When creator removes instance context should be acknowledged.

    If creator removes instance context should know about it to avoid
    possible issues in the session.

    Args:
        instance (CreatedInstance): Object of instance which was removed
            from scene metadata.
    """

    self._remove_instances([instance])

execute_autocreators()

Execute discovered AutoCreator plugins.

Reset instances if any autocreator executed properly.

Source code in client/ayon_core/pipeline/create/context.py
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
def execute_autocreators(self):
    """Execute discovered AutoCreator plugins.

    Reset instances if any autocreator executed properly.
    """

    failed_info = []
    for creator in self.sorted_autocreators:
        identifier = creator.identifier
        _, fail_info = self._create_with_unified_error(identifier, creator)
        if fail_info is not None:
            failed_info.append(fail_info)

    if failed_info:
        raise CreatorsCreateFailed(failed_info)

find_convertor_items()

Go through convertor plugins to look for items to convert.

Raises:

Type Description
ConvertorsFindFailed

When one or more convertors fails during finding.

Source code in client/ayon_core/pipeline/create/context.py
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
def find_convertor_items(self):
    """Go through convertor plugins to look for items to convert.

    Raises:
        ConvertorsFindFailed: When one or more convertors fails during
            finding.
    """

    self.convertor_items_by_id = {}

    failed_info = []
    for convertor in self.convertors_plugins.values():
        try:
            convertor.find_instances()

        except:  # noqa: E722
            failed_info.append(
                prepare_failed_convertor_operation_info(
                    convertor.identifier, sys.exc_info()
                )
            )
            self.log.warning(
                "Failed to find instances of convertor \"{}\"".format(
                    convertor.identifier
                ),
                exc_info=True
            )

    if failed_info:
        raise ConvertorsFindFailed(failed_info)

get_current_folder_entity()

Folder entity for current context folder.

Returns:

Type Description
Optional[Dict[str, Any]]

Optional[dict[str, Any]]: Folder entity.

Source code in client/ayon_core/pipeline/create/context.py
426
427
428
429
430
431
432
433
434
435
436
437
438
def get_current_folder_entity(self) -> Optional[Dict[str, Any]]:
    """Folder entity for current context folder.

    Returns:
        Optional[dict[str, Any]]: Folder entity.

    """
    if self._current_folder_entity is not _NOT_SET:
        return copy.deepcopy(self._current_folder_entity)

    folder_path = self.get_current_folder_path()
    self._current_folder_entity = self.get_folder_entity(folder_path)
    return copy.deepcopy(self._current_folder_entity)

get_current_folder_path()

Folder path which was used as current context on context reset.

Returns:

Type Description
Optional[str]

Union[str, None]: Folder path.

Source code in client/ayon_core/pipeline/create/context.py
377
378
379
380
381
382
383
384
def get_current_folder_path(self) -> Optional[str]:
    """Folder path which was used as current context on context reset.

    Returns:
        Union[str, None]: Folder path.
    """

    return self._current_folder_path

get_current_project_anatomy()

Project anatomy for current project.

Returns:

Name Type Description
Anatomy

Anatomy object ready to be used.

Source code in client/ayon_core/pipeline/create/context.py
466
467
468
469
470
471
472
473
474
475
476
def get_current_project_anatomy(self):
    """Project anatomy for current project.

    Returns:
        Anatomy: Anatomy object ready to be used.
    """

    if self._current_project_anatomy is None:
        self._current_project_anatomy = Anatomy(
            self._current_project_name)
    return self._current_project_anatomy

get_current_project_entity()

Project entity for current context project.

Returns:

Type Description
Optional[Dict[str, Any]]

Union[dict[str, Any], None]: Folder entity.

Source code in client/ayon_core/pipeline/create/context.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
def get_current_project_entity(self) -> Optional[Dict[str, Any]]:
    """Project entity for current context project.

    Returns:
        Union[dict[str, Any], None]: Folder entity.

    """
    if self._current_project_entity is not _NOT_SET:
        return copy.deepcopy(self._current_project_entity)
    project_entity = None
    project_name = self.get_current_project_name()
    if project_name:
        project_entity = ayon_api.get_project(project_name)
    self._current_project_entity = project_entity
    return copy.deepcopy(self._current_project_entity)

get_current_project_name()

Project name which was used as current context on context reset.

Returns:

Type Description
Optional[str]

Union[str, None]: Project name.

Source code in client/ayon_core/pipeline/create/context.py
368
369
370
371
372
373
374
375
def get_current_project_name(self) -> Optional[str]:
    """Project name which was used as current context on context reset.

    Returns:
        Union[str, None]: Project name.
    """

    return self._current_project_name

get_current_task_entity()

Task entity for current context task.

Returns:

Type Description
Optional[Dict[str, Any]]

Union[dict[str, Any], None]: Task entity.

Source code in client/ayon_core/pipeline/create/context.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def get_current_task_entity(self) -> Optional[Dict[str, Any]]:
    """Task entity for current context task.

    Returns:
        Union[dict[str, Any], None]: Task entity.

    """
    if self._current_task_entity is not _NOT_SET:
        return copy.deepcopy(self._current_task_entity)

    folder_path = self.get_current_folder_path()
    task_name = self.get_current_task_name()
    self._current_task_entity = self.get_task_entity(
        folder_path, task_name
    )
    return copy.deepcopy(self._current_task_entity)

get_current_task_name()

Task name which was used as current context on context reset.

Returns:

Type Description
Optional[str]

Union[str, None]: Task name.

Source code in client/ayon_core/pipeline/create/context.py
386
387
388
389
390
391
392
393
def get_current_task_name(self) -> Optional[str]:
    """Task name which was used as current context on context reset.

    Returns:
        Union[str, None]: Task name.
    """

    return self._current_task_name

get_current_task_type()

Task type which was used as current context on context reset.

Returns:

Type Description
Optional[str]

Union[str, None]: Task type.

Source code in client/ayon_core/pipeline/create/context.py
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def get_current_task_type(self) -> Optional[str]:
    """Task type which was used as current context on context reset.

    Returns:
        Union[str, None]: Task type.

    """
    if self._current_task_type is _NOT_SET:
        task_type = None
        task_entity = self.get_current_task_entity()
        if task_entity:
            task_type = task_entity["taskType"]
        self._current_task_type = task_type
    return self._current_task_type

get_current_workfile_path()

Workfile path which was opened on context reset.

Returns:

Type Description

Union[str, None]: Workfile path.

Source code in client/ayon_core/pipeline/create/context.py
457
458
459
460
461
462
463
464
def get_current_workfile_path(self):
    """Workfile path which was opened on context reset.

    Returns:
        Union[str, None]: Workfile path.
    """

    return self._current_workfile_path

get_folder_entities(folder_paths)

Get folder entities by paths.

Parameters:

Name Type Description Default
folder_paths Iterable[str]

Folder paths.

required

Returns:

Type Description

Dict[str, Optional[Dict[str, Any]]]: Folder entities by path.

Source code in client/ayon_core/pipeline/create/context.py
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
def get_folder_entities(self, folder_paths: Iterable[str]):
    """Get folder entities by paths.

    Args:
        folder_paths (Iterable[str]): Folder paths.

    Returns:
        Dict[str, Optional[Dict[str, Any]]]: Folder entities by path.

    """
    output = {
        folder_path: None
        for folder_path in folder_paths
    }
    remainder_paths = set()
    for folder_path in output:
        # Skip invalid folder paths (folder name or empty path)
        if not folder_path or "/" not in folder_path:
            continue

        if folder_path not in self._folder_entities_by_path:
            remainder_paths.add(folder_path)
            continue

        output[folder_path] = self._folder_entities_by_path[folder_path]

    if not remainder_paths:
        return output

    found_paths = set()
    for folder_entity in ayon_api.get_folders(
        self.project_name,
        folder_paths=remainder_paths,
    ):
        folder_path = folder_entity["path"]
        found_paths.add(folder_path)
        output[folder_path] = folder_entity
        self._folder_entities_by_path[folder_path] = folder_entity

    # Cache empty folder entities
    for path in remainder_paths - found_paths:
        self._folder_entities_by_path[path] = None

    return output

get_folder_entity(folder_path)

Get folder entity by path.

Entities are cached until reset.

Parameters:

Name Type Description Default
folder_path Optional[str]

Folder path.

required

Returns:

Type Description
Optional[Dict[str, Any]]

Optional[Dict[str, Any]]: Folder entity.

Source code in client/ayon_core/pipeline/create/context.py
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
def get_folder_entity(
    self,
    folder_path: Optional[str],
) -> Optional[Dict[str, Any]]:
    """Get folder entity by path.

    Entities are cached until reset.

    Args:
        folder_path (Optional[str]): Folder path.

    Returns:
        Optional[Dict[str, Any]]: Folder entity.

    """
    if not folder_path:
        return None
    return self.get_folder_entities([folder_path]).get(folder_path)

get_host_misssing_methods(host) classmethod

Collect missing methods from host.

Parameters:

Name Type Description Default
host(ModuleType)

Host implementaion.

required
Source code in client/ayon_core/pipeline/create/context.py
344
345
346
347
348
349
350
351
352
353
354
355
@classmethod
def get_host_misssing_methods(cls, host):
    """Collect missing methods from host.

    Args:
        host(ModuleType): Host implementaion.
    """

    missing = set(
        IPublishHost.get_missing_publish_methods(host)
    )
    return missing

get_instance_by_id(instance_id)

Receive instance by id.

Parameters:

Name Type Description Default
instance_id str

Instance id.

required

Returns:

Type Description
Optional[CreatedInstance]

Optional[CreatedInstance]: Instance or None if instance with given id is not available.

Source code in client/ayon_core/pipeline/create/context.py
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def get_instance_by_id(
    self, instance_id: str
) -> Optional["CreatedInstance"]:
    """Receive instance by id.

    Args:
        instance_id (str): Instance id.

    Returns:
        Optional[CreatedInstance]: Instance or None if instance with
            given id is not available.

    """
    return self._instances_by_id.get(instance_id)

get_instances_context_info(instances=None)

Validate 'folder' and 'task' instance context.

Parameters:

Name Type Description Default
instances Optional[Iterable[CreatedInstance]]

Instances to validate. If not provided all instances are validated.

None

Returns:

Type Description
Dict[str, InstanceContextInfo]

Dict[str, InstanceContextInfo]: Validation results by instance id.

Source code in client/ayon_core/pipeline/create/context.py
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
def get_instances_context_info(
    self, instances: Optional[Iterable["CreatedInstance"]] = None
) -> Dict[str, InstanceContextInfo]:
    """Validate 'folder' and 'task' instance context.

    Args:
        instances (Optional[Iterable[CreatedInstance]]): Instances to
            validate. If not provided all instances are validated.

    Returns:
        Dict[str, InstanceContextInfo]: Validation results by instance id.

    """
    # Use all instances from context if 'instances' are not passed
    if instances is None:
        instances = self._instances_by_id.values()
    instances = tuple(instances)
    info_by_instance_id = {
        instance.id: InstanceContextInfo(
            instance.get("folderPath"),
            instance.get("task"),
            False,
            False,
        )
        for instance in instances
    }

    # Skip if instances are empty
    if not info_by_instance_id:
        return info_by_instance_id

    project_name = self.project_name

    to_validate = []
    task_names_by_folder_path = collections.defaultdict(set)
    for instance in instances:
        context_info = info_by_instance_id[instance.id]
        if instance.has_promised_context:
            context_info.folder_is_valid = True
            context_info.task_is_valid = True
            # NOTE missing task type
            continue
        # TODO allow context promise
        folder_path = context_info.folder_path
        if not folder_path:
            continue

        if folder_path in self._folder_entities_by_path:
            folder_entity = self._folder_entities_by_path[folder_path]
            if folder_entity is None:
                continue
            context_info.folder_is_valid = True

        task_name = context_info.task_name
        if task_name is not None:
            tasks_cache = self._task_names_by_folder_path.get(folder_path)
            if tasks_cache is not None:
                context_info.task_is_valid = task_name in tasks_cache
                continue

        to_validate.append(instance)
        task_names_by_folder_path[folder_path].add(task_name)

    if not to_validate:
        return info_by_instance_id

    # Backwards compatibility for cases where folder name is set instead
    #   of folder path
    folder_paths = set()
    task_names_by_folder_name = {}
    task_names_by_folder_path_clean = {}
    for folder_path, task_names in task_names_by_folder_path.items():
        if folder_path is None:
            continue

        clean_task_names = {
            task_name
            for task_name in task_names
            if task_name
        }

        if "/" not in folder_path:
            task_names_by_folder_name[folder_path] = clean_task_names
            continue

        folder_paths.add(folder_path)
        if not clean_task_names:
            continue

        task_names_by_folder_path_clean[folder_path] = clean_task_names

    folder_paths_by_name = collections.defaultdict(list)
    if task_names_by_folder_name:
        for folder_entity in ayon_api.get_folders(
            project_name,
            folder_names=task_names_by_folder_name.keys(),
            fields={"name", "path"}
        ):
            folder_name = folder_entity["name"]
            folder_path = folder_entity["path"]
            folder_paths_by_name[folder_name].append(folder_path)

    folder_path_by_name = {}
    for folder_name, paths in folder_paths_by_name.items():
        if len(paths) != 1:
            continue
        path = paths[0]
        folder_path_by_name[folder_name] = path
        folder_paths.add(path)
        clean_task_names = task_names_by_folder_name[folder_name]
        if not clean_task_names:
            continue
        folder_task_names = task_names_by_folder_path_clean.setdefault(
            path, set()
        )
        folder_task_names |= clean_task_names

    folder_entities_by_path = self.get_folder_entities(folder_paths)
    task_entities_by_folder_path = self.get_task_entities(
        task_names_by_folder_path_clean
    )

    for instance in to_validate:
        folder_path = instance["folderPath"]
        task_name = instance.get("task")
        if folder_path and "/" not in folder_path:
            new_folder_path = folder_path_by_name.get(folder_path)
            if new_folder_path:
                folder_path = new_folder_path
                instance["folderPath"] = new_folder_path

        folder_entity = folder_entities_by_path.get(folder_path)
        if not folder_entity:
            continue
        context_info = info_by_instance_id[instance.id]
        context_info.folder_is_valid = True

        if (
            not task_name
            or task_name in task_entities_by_folder_path[folder_path]
        ):
            context_info.task_is_valid = True
    return info_by_instance_id

get_instances_task_entities(instances=None)

Get task entities for instances.

Parameters:

Name Type Description Default
instances Optional[Iterable[CreatedInstance]]

Instances to get task entities. If not provided all instances are used.

None

Returns:

Type Description

Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id.

Source code in client/ayon_core/pipeline/create/context.py
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
def get_instances_task_entities(
    self, instances: Optional[Iterable["CreatedInstance"]] = None
):
    """Get task entities for instances.

    Args:
        instances (Optional[Iterable[CreatedInstance]]): Instances to
            get task entities. If not provided all instances are used.

    Returns:
        Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id.

    """
    if instances is None:
        instances = self._instances_by_id.values()
    instances = list(instances)

    output = {
        instance.id: None
        for instance in instances
    }
    if not instances:
        return output

    filtered_instances = []
    task_names_by_folder_path = collections.defaultdict(set)
    for instance in instances:
        folder_path = instance.get("folderPath")
        task_name = instance.get("task")
        if not folder_path or not task_name:
            continue
        filtered_instances.append(instance)
        task_names_by_folder_path[folder_path].add(task_name)

    task_entities_by_folder_path = self.get_task_entities(
        task_names_by_folder_path
    )
    for instance in filtered_instances:
        folder_path = instance["folderPath"]
        task_name = instance["task"]
        output[instance.id] = (
            task_entities_by_folder_path[folder_path][task_name]
        )

    return output

get_sorted_creators(identifiers=None)

Sorted creators by 'order' attribute.

Parameters:

Name Type Description Default
identifiers Iterable[str]

Filter creators by identifiers. All creators are returned if 'None' is passed.

None

Returns:

Type Description

List[BaseCreator]: Sorted creator plugins by 'order' value.

Source code in client/ayon_core/pipeline/create/context.py
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def get_sorted_creators(self, identifiers=None):
    """Sorted creators by 'order' attribute.

    Args:
        identifiers (Iterable[str]): Filter creators by identifiers. All
            creators are returned if 'None' is passed.

    Returns:
        List[BaseCreator]: Sorted creator plugins by 'order' value.

    """
    if identifiers is not None:
        identifiers = set(identifiers)
        creators = [
            creator
            for identifier, creator in self.creators.items()
            if identifier in identifiers
        ]
    else:
        creators = self.creators.values()

    return sorted(
        creators, key=lambda creator: creator.order
    )

get_task_entities(task_names_by_folder_paths)

Get task entities by folder path and task name.

Entities are cached until reset.

Parameters:

Name Type Description Default
task_names_by_folder_paths Dict[str, Set[str]]

Task names by folder path.

required

Returns:

Type Description
Dict[str, Dict[str, Optional[Dict[str, Any]]]]

Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path and task name.

Source code in client/ayon_core/pipeline/create/context.py
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
def get_task_entities(
    self,
    task_names_by_folder_paths: Dict[str, Set[str]]
) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]:
    """Get task entities by folder path and task name.

    Entities are cached until reset.

    Args:
        task_names_by_folder_paths (Dict[str, Set[str]]): Task names by
            folder path.

    Returns:
        Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path
            and task name.

    """
    output = {}
    for folder_path, task_names in task_names_by_folder_paths.items():
        if folder_path is None:
            continue
        output[folder_path] = {
            task_name: None
            for task_name in task_names
            if task_name is not None
        }

    missing_folder_paths = set()
    for folder_path, output_task_entities_by_name in output.items():
        if not output_task_entities_by_name:
            continue

        if folder_path not in self._task_ids_by_folder_path:
            missing_folder_paths.add(folder_path)
            continue

        all_tasks_filled = True
        task_ids = self._task_ids_by_folder_path[folder_path]
        task_entities_by_name = {}
        for task_id in task_ids:
            task_entity = self._task_entities_by_id.get(task_id)
            if task_entity is None:
                all_tasks_filled = False
                continue
            task_entities_by_name[task_entity["name"]] = task_entity

        any_missing = False
        for task_name in set(output_task_entities_by_name):
            task_entity = task_entities_by_name.get(task_name)
            if task_entity is None:
                any_missing = True
                continue

            output_task_entities_by_name[task_name] = task_entity

        if any_missing and not all_tasks_filled:
            missing_folder_paths.add(folder_path)

    if not missing_folder_paths:
        return output

    folder_entities_by_path = self.get_folder_entities(
        missing_folder_paths
    )
    folder_path_by_id = {}
    for folder_path, folder_entity in folder_entities_by_path.items():
        if folder_entity is not None:
            folder_path_by_id[folder_entity["id"]] = folder_path

    if not folder_path_by_id:
        return output

    task_entities_by_parent_id = collections.defaultdict(list)
    for task_entity in ayon_api.get_tasks(
        self.project_name,
        folder_ids=folder_path_by_id.keys()
    ):
        folder_id = task_entity["folderId"]
        task_entities_by_parent_id[folder_id].append(task_entity)

    for folder_id, task_entities in task_entities_by_parent_id.items():
        folder_path = folder_path_by_id[folder_id]
        task_ids = set()
        task_names = set()
        for task_entity in task_entities:
            task_id = task_entity["id"]
            task_name = task_entity["name"]
            task_ids.add(task_id)
            task_names.add(task_name)
            self._task_entities_by_id[task_id] = task_entity

            output[folder_path][task_name] = task_entity
        self._task_ids_by_folder_path[folder_path] = task_ids
        self._task_names_by_folder_path[folder_path] = task_names

    return output

get_task_entity(folder_path, task_name)

Get task entity by name and folder path.

Entities are cached until reset.

Parameters:

Name Type Description Default
folder_path Optional[str]

Folder path.

required
task_name Optional[str]

Task name.

required

Returns:

Type Description
Optional[Dict[str, Any]]

Optional[Dict[str, Any]]: Task entity.

Source code in client/ayon_core/pipeline/create/context.py
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
def get_task_entity(
    self,
    folder_path: Optional[str],
    task_name: Optional[str],
) -> Optional[Dict[str, Any]]:
    """Get task entity by name and folder path.

    Entities are cached until reset.

    Args:
        folder_path (Optional[str]): Folder path.
        task_name (Optional[str]): Task name.

    Returns:
        Optional[Dict[str, Any]]: Task entity.

    """
    if not folder_path or not task_name:
        return None

    output = self.get_task_entities({folder_path: {task_name}})
    return output.get(folder_path, {}).get(task_name)

get_template_data(folder_path, task_name)

Prepare template data for given context.

Method is using cached entities and settings to prepare template data.

Parameters:

Name Type Description Default
folder_path Optional[str]

Folder path.

required
task_name Optional[str]

Task name.

required

Returns:

Type Description
Dict[str, Any]

dict[str, Any]: Template data.

Source code in client/ayon_core/pipeline/create/context.py
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
def get_template_data(
    self, folder_path: Optional[str], task_name: Optional[str]
) -> Dict[str, Any]:
    """Prepare template data for given context.

    Method is using cached entities and settings to prepare template data.

    Args:
        folder_path (Optional[str]): Folder path.
        task_name (Optional[str]): Task name.

    Returns:
        dict[str, Any]: Template data.

    """
    project_entity = self.get_current_project_entity()
    folder_entity = task_entity = None
    if folder_path:
        folder_entity = self.get_folder_entity(folder_path)
        if task_name and folder_entity:
            task_entity = self.get_task_entity(folder_path, task_name)

    return get_template_data(
        project_entity,
        folder_entity,
        task_entity,
        host_name=self.host_name,
        settings=self.get_current_project_settings(),
    )

instance_create_attr_defs_changed(instance_id)

Instance attribute definitions changed.

Triggered by instance 'CreatorAttributeValues' on instance.

Parameters:

Name Type Description Default
instance_id str

Instance id.

required
Source code in client/ayon_core/pipeline/create/context.py
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
def instance_create_attr_defs_changed(self, instance_id: str):
    """Instance attribute definitions changed.

    Triggered by instance 'CreatorAttributeValues' on instance.

    Args:
        instance_id (str): Instance id.

    """
    if self._is_instance_events_ready(instance_id):
        with self.bulk_create_attr_defs_change() as bulk_item:
            bulk_item.append(instance_id)

instance_publish_attr_defs_changed(instance_id, plugin_name)

Instance attribute definitions changed.

Triggered by instance 'PublishAttributeValues' on instance.

Parameters:

Name Type Description Default
instance_id Optional[str]

Instance id or None for context.

required
plugin_name str

Plugin name which attribute definitions were changed.

required
Source code in client/ayon_core/pipeline/create/context.py
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
def instance_publish_attr_defs_changed(
    self, instance_id: Optional[str], plugin_name: str
):
    """Instance attribute definitions changed.

    Triggered by instance 'PublishAttributeValues' on instance.

    Args:
        instance_id (Optional[str]): Instance id or None for context.
        plugin_name (str): Plugin name which attribute definitions were
            changed.

    """
    if self._is_instance_events_ready(instance_id):
        with self.bulk_publish_attr_defs_change() as bulk_item:
            bulk_item.append((instance_id, plugin_name))

instance_values_changed(instance_id, new_values)

Instance value changed.

Triggered by `CreatedInstance, 'CreatorAttributeValues' or 'PublishAttributeValues' on instance.

Parameters:

Name Type Description Default
instance_id Optional[str]

Instance id or None for context.

required
new_values Dict[str, Any]

Changed values.

required
Source code in client/ayon_core/pipeline/create/context.py
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
def instance_values_changed(
    self, instance_id: Optional[str], new_values: Dict[str, Any]
):
    """Instance value changed.

    Triggered by `CreatedInstance, 'CreatorAttributeValues'
        or 'PublishAttributeValues' on instance.

    Args:
        instance_id (Optional[str]): Instance id or None for context.
        new_values (Dict[str, Any]): Changed values.

    """
    if self._is_instance_events_ready(instance_id):
        with self.bulk_value_changes() as bulk_item:
            bulk_item.append((instance_id, new_values))

publish_attribute_value_changed(plugin_name, value)

Context publish attribute values changed.

Triggered by instance 'PublishAttributeValues' on context.

Parameters:

Name Type Description Default
plugin_name str

Plugin name which changed value.

required
value Dict[str, Any]

Changed values.

required
Source code in client/ayon_core/pipeline/create/context.py
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
def publish_attribute_value_changed(
    self, plugin_name: str, value: Dict[str, Any]
):
    """Context publish attribute values changed.

    Triggered by instance 'PublishAttributeValues' on context.

    Args:
        plugin_name (str): Plugin name which changed value.
        value (Dict[str, Any]): Changed values.

    """
    self.instance_values_changed(
        None,
        {
            "publish_attributes": {
                plugin_name: value,
            },
        },
    )

refresh_thumbnails()

Cleanup thumbnail paths.

Remove all thumbnail filepaths that are empty or lead to files which does not exist or of instances that are not available anymore.

Source code in client/ayon_core/pipeline/create/context.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
def refresh_thumbnails(self):
    """Cleanup thumbnail paths.

    Remove all thumbnail filepaths that are empty or lead to files which
    does not exist or of instances that are not available anymore.
    """

    invalid = set()
    for instance_id, path in self.thumbnail_paths_by_instance_id.items():
        instance_available = True
        if instance_id is not None:
            instance_available = instance_id in self._instances_by_id

        if (
            not instance_available
            or not path
            or not os.path.exists(path)
        ):
            invalid.add(instance_id)

    for instance_id in invalid:
        self.thumbnail_paths_by_instance_id.pop(instance_id)

remove_instances(instances, sender=None)

Remove instances from context.

All instances that don't have creator identifier leading to existing creator are just removed from context.

Parameters:

Name Type Description Default
instances List[CreatedInstance]

Instances that should be removed. Remove logic is done using creator, which may require to do other cleanup than just remove instance from context.

required
sender Optional[str]

Sender of the event.

None
Source code in client/ayon_core/pipeline/create/context.py
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
def remove_instances(self, instances, sender=None):
    """Remove instances from context.

    All instances that don't have creator identifier leading to existing
        creator are just removed from context.

    Args:
        instances (List[CreatedInstance]): Instances that should be
            removed. Remove logic is done using creator, which may require
            to do other cleanup than just remove instance from context.
        sender (Optional[str]): Sender of the event.

    """
    instances_by_identifier = collections.defaultdict(list)
    for instance in instances:
        identifier = instance.creator_identifier
        instances_by_identifier[identifier].append(instance)

    # Just remove instances from context if creator is not available
    missing_creators = set(instances_by_identifier) - set(self.creators)
    instances = []
    for identifier in missing_creators:
        instances.extend(
            instance
            for instance in instances_by_identifier[identifier]
        )

    self._remove_instances(instances, sender)

    error_message = "Instances removement of creator \"{}\" failed. {}"
    failed_info = []
    # Remove instances by creator plugin order
    for creator in self.get_sorted_creators(
        instances_by_identifier.keys()
    ):
        identifier = creator.identifier
        creator_instances = instances_by_identifier[identifier]

        label = creator.label
        failed = False
        add_traceback = False
        exc_info = None
        try:
            creator.remove_instances(creator_instances)

        except CreatorError:
            failed = True
            exc_info = sys.exc_info()
            self.log.warning(
                error_message.format(identifier, exc_info[1])
            )

        except (KeyboardInterrupt, SystemExit):
            raise

        except:  # noqa: E722
            failed = True
            add_traceback = True
            exc_info = sys.exc_info()
            self.log.warning(
                error_message.format(identifier, ""),
                exc_info=True
            )

        if failed:
            failed_info.append(
                prepare_failed_creator_operation_info(
                    identifier, label, exc_info, add_traceback
                )
            )

    if failed_info:
        raise CreatorsRemoveFailed(failed_info)

reset(discover_publish_plugins=True)

Reset context with all plugins and instances.

All changes will be lost if were not saved explicitely.

Source code in client/ayon_core/pipeline/create/context.py
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def reset(self, discover_publish_plugins=True):
    """Reset context with all plugins and instances.

    All changes will be lost if were not saved explicitely.
    """

    self.reset_preparation()

    self.reset_current_context()
    self.reset_plugins(discover_publish_plugins)
    self.reset_context_data()

    with self.bulk_add_instances():
        self.reset_instances()
        self.find_convertor_items()
        self.execute_autocreators()

    self.reset_finalization()

reset_context_data()

Reload context data using host implementation.

These data are not related to any instance but may be needed for whole publishing.

Source code in client/ayon_core/pipeline/create/context.py
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
def reset_context_data(self):
    """Reload context data using host implementation.

    These data are not related to any instance but may be needed for whole
    publishing.
    """
    if not self.host_is_valid:
        self._original_context_data = {}
        self._publish_attributes = PublishAttributes(self, {})
        return

    original_data = self.host.get_context_data() or {}
    self._original_context_data = copy.deepcopy(original_data)

    publish_attributes = original_data.get("publish_attributes") or {}

    self._publish_attributes = PublishAttributes(
        self, publish_attributes
    )

    for plugin in self.plugins_with_defs:
        if is_func_signature_supported(
            plugin.convert_attribute_values, self, None
        ):
            plugin.convert_attribute_values(self, None)

        elif not plugin.__instanceEnabled__:
            output = plugin.convert_attribute_values(publish_attributes)
            if output:
                publish_attributes.update(output)

    for plugin in self.plugins_with_defs:
        attr_defs = plugin.get_attr_defs_for_context(self)
        if not attr_defs:
            continue
        self._publish_attributes.set_publish_plugin_attr_defs(
            plugin.__name__, attr_defs
        )

reset_current_context()

Refresh current context.

Reset is based on optional host implementation of get_current_context function.

Some hosts have ability to change context file without using workfiles tool but that change is not propagated to 'os.environ'.

Todos

UI: Current context should be also checked on save - compare initial values vs. current values. Related to UI checks: Current workfile can be also considered as current context information as that's where the metadata are stored. We should store the workfile (if is available) too.

Source code in client/ayon_core/pipeline/create/context.py
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
def reset_current_context(self):
    """Refresh current context.

    Reset is based on optional host implementation of `get_current_context`
    function.

    Some hosts have ability to change context file without using workfiles
    tool but that change is not propagated to 'os.environ'.

    Todos:
        UI: Current context should be also checked on save - compare
            initial values vs. current values.
        Related to UI checks: Current workfile can be also considered
            as current context information as that's where the metadata
            are stored. We should store the workfile (if is available) too.
    """

    project_name, folder_path, task_name, workfile_path = (
        self._get_current_host_context()
    )

    self._current_project_name = project_name
    self._current_folder_path = folder_path
    self._current_task_name = task_name
    self._current_workfile_path = workfile_path

    self._current_project_entity = _NOT_SET
    self._current_folder_entity = _NOT_SET
    self._current_task_entity = _NOT_SET
    self._current_task_type = _NOT_SET

    self._current_project_anatomy = None
    self._current_project_settings = None

reset_finalization()

Cleanup of attributes after reset.

Source code in client/ayon_core/pipeline/create/context.py
601
602
603
604
605
606
def reset_finalization(self):
    """Cleanup of attributes after reset."""

    # Stop access to collection shared data
    self._collection_shared_data = None
    self.refresh_thumbnails()

reset_instances()

Reload instances

Source code in client/ayon_core/pipeline/create/context.py
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
def reset_instances(self):
    """Reload instances"""
    self._instances_by_id = collections.OrderedDict()

    # Collect instances
    error_message = "Collection of instances for creator {} failed. {}"
    failed_info = []
    for creator in self.sorted_creators:
        label = creator.label
        identifier = creator.identifier
        failed = False
        add_traceback = False
        exc_info = None
        try:
            creator.collect_instances()

        except CreatorError:
            failed = True
            exc_info = sys.exc_info()
            self.log.warning(error_message.format(identifier, exc_info[1]))

        except:  # noqa: E722
            failed = True
            add_traceback = True
            exc_info = sys.exc_info()
            self.log.warning(
                error_message.format(identifier, ""),
                exc_info=True
            )

        if failed:
            failed_info.append(
                prepare_failed_creator_operation_info(
                    identifier, label, exc_info, add_traceback
                )
            )

    if failed_info:
        raise CreatorsCollectionFailed(failed_info)

reset_plugins(discover_publish_plugins=True)

Reload plugins.

Reloads creators from preregistered paths and can load publish plugins if it's enabled on context.

Source code in client/ayon_core/pipeline/create/context.py
656
657
658
659
660
661
662
663
664
665
def reset_plugins(self, discover_publish_plugins=True):
    """Reload plugins.

    Reloads creators from preregistered paths and can load publish plugins
    if it's enabled on context.
    """

    self._reset_publish_plugins(discover_publish_plugins)
    self._reset_creator_plugins()
    self._reset_convertor_plugins()

reset_preparation()

Prepare attributes that must be prepared/cleaned before reset.

Source code in client/ayon_core/pipeline/create/context.py
587
588
589
590
591
592
593
594
595
596
597
598
599
def reset_preparation(self):
    """Prepare attributes that must be prepared/cleaned before reset."""

    # Give ability to store shared data for collection phase
    self._collection_shared_data = {}

    self._folder_entities_by_path = {}
    self._task_entities_by_id = {}

    self._task_ids_by_folder_path = {}
    self._task_names_by_folder_path = {}

    self._event_hub.clear_callbacks()

run_convertor(convertor_identifier)

Run convertor plugin by identifier.

Conversion is skipped if convertor is not available.

Parameters:

Name Type Description Default
convertor_identifier str

Identifier of convertor.

required
Source code in client/ayon_core/pipeline/create/context.py
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
def run_convertor(self, convertor_identifier):
    """Run convertor plugin by identifier.

    Conversion is skipped if convertor is not available.

    Args:
        convertor_identifier (str): Identifier of convertor.
    """

    convertor = self.convertors_plugins.get(convertor_identifier)
    if convertor is not None:
        convertor.convert()

run_convertors(convertor_identifiers)

Run convertor plugins by identifiers.

Conversion is skipped if convertor is not available. It is recommended to trigger reset after conversion to reload instances.

Parameters:

Name Type Description Default
convertor_identifiers Iterator[str]

Identifiers of convertors to run.

required

Raises:

Type Description
ConvertorsConversionFailed

When one or more convertors fails.

Source code in client/ayon_core/pipeline/create/context.py
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
def run_convertors(self, convertor_identifiers):
    """Run convertor plugins by identifiers.

    Conversion is skipped if convertor is not available. It is recommended
    to trigger reset after conversion to reload instances.

    Args:
        convertor_identifiers (Iterator[str]): Identifiers of convertors
            to run.

    Raises:
        ConvertorsConversionFailed: When one or more convertors fails.
    """

    failed_info = []
    for convertor_identifier in convertor_identifiers:
        try:
            self.run_convertor(convertor_identifier)

        except:  # noqa: E722
            failed_info.append(
                prepare_failed_convertor_operation_info(
                    convertor_identifier, sys.exc_info()
                )
            )
            self.log.warning(
                "Failed to convert instances of convertor \"{}\"".format(
                    convertor_identifier
                ),
                exc_info=True
            )

    if failed_info:
        raise ConvertorsConversionFailed(failed_info)

save_changes()

Save changes. Update all changed values.

Source code in client/ayon_core/pipeline/create/context.py
1865
1866
1867
1868
1869
1870
1871
1872
def save_changes(self):
    """Save changes. Update all changed values."""
    if not self.host_is_valid:
        missing_methods = self.get_host_misssing_methods(self.host)
        raise HostMissRequiredMethod(self.host, missing_methods)

    self._save_context_changes()
    self._save_instance_changes()

set_context_publish_plugin_attr_defs(plugin_name, attr_defs)

Set attribute definitions for CreateContext publish plugin.

Parameters:

Name Type Description Default
plugin_name(str)

Name of publish plugin.

required
attr_defs(List[AbstractAttrDef])

Attribute definitions.

required
Source code in client/ayon_core/pipeline/create/context.py
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
def set_context_publish_plugin_attr_defs(self, plugin_name, attr_defs):
    """Set attribute definitions for CreateContext publish plugin.

    Args:
        plugin_name(str): Name of publish plugin.
        attr_defs(List[AbstractAttrDef]): Attribute definitions.

    """
    self.publish_attributes.set_publish_plugin_attr_defs(
        plugin_name, attr_defs
    )
    self.instance_publish_attr_defs_changed(
        None, plugin_name
    )

CreatedInstance

Instance entity with data that will be stored to workfile.

I think data must be required argument containing all minimum information about instance like "folderPath" and "task" and all data used for filling product name as creators may have custom data for product name filling.

Notes

Object have 2 possible initialization. One using 'creator' object which is recommended for api usage. Second by passing information about creator.

Parameters:

Name Type Description Default
product_type str

Product type that will be created.

required
product_name str

Name of product that will be created.

required
data Dict[str, Any]

Data used for filling product name or override data from already existing instance.

required
creator BaseCreator

Creator responsible for instance.

required
Source code in client/ayon_core/pipeline/create/structures.py
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
class CreatedInstance:
    """Instance entity with data that will be stored to workfile.

    I think `data` must be required argument containing all minimum information
    about instance like "folderPath" and "task" and all data used for filling
    product name as creators may have custom data for product name filling.

    Notes:
        Object have 2 possible initialization. One using 'creator' object which
            is recommended for api usage. Second by passing information about
            creator.

    Args:
        product_type (str): Product type that will be created.
        product_name (str): Name of product that will be created.
        data (Dict[str, Any]): Data used for filling product name or override
            data from already existing instance.
        creator (BaseCreator): Creator responsible for instance.
    """

    # Keys that can't be changed or removed from data after loading using
    #   creator.
    # - 'creator_attributes' and 'publish_attributes' can change values of
    #   their individual children but not on their own
    __immutable_keys = (
        "id",
        "instance_id",
        "productType",
        "creator_identifier",
        "creator_attributes",
        "publish_attributes"
    )
    # Keys that can be changed, but should not be removed from instance
    __required_keys = {
        "folderPath": None,
        "task": None,
        "productName": None,
        "active": True,
    }

    def __init__(
        self,
        product_type: str,
        product_name: str,
        data: Dict[str, Any],
        creator: "BaseCreator",
        transient_data: Optional[Dict[str, Any]] = None,
    ):
        self._creator = creator
        creator_identifier = creator.identifier
        group_label = creator.get_group_label()
        creator_label = creator.label

        self._creator_label = creator_label
        self._group_label = group_label or creator_identifier

        # Instance members may have actions on them
        # TODO implement members logic
        self._members = []

        # Data that can be used for lifetime of object
        if transient_data is None:
            transient_data = {}
        self._transient_data = transient_data

        # Create a copy of passed data to avoid changing them on the fly
        data = copy.deepcopy(data or {})

        # Pop dictionary values that will be converted to objects to be able
        #   catch changes
        orig_creator_attributes = data.pop("creator_attributes", None) or {}
        orig_publish_attributes = data.pop("publish_attributes", None) or {}

        # Store original value of passed data
        self._orig_data = copy.deepcopy(data)

        # Pop 'productType' and 'productName' to prevent unexpected changes
        data.pop("productType", None)
        data.pop("productName", None)
        # Backwards compatibility with OpenPype instances
        data.pop("family", None)
        data.pop("subset", None)

        asset_name = data.pop("asset", None)
        if "folderPath" not in data:
            data["folderPath"] = asset_name

        # QUESTION Does it make sense to have data stored as ordered dict?
        self._data = collections.OrderedDict()
        # QUESTION Do we need this "id" information on instance?
        item_id = data.get("id")
        # TODO use only 'AYON_INSTANCE_ID' when all hosts support it
        if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}:
            item_id = AYON_INSTANCE_ID
        self._data["id"] = item_id
        self._data["productType"] = product_type
        self._data["productName"] = product_name
        self._data["active"] = data.get("active", True)
        self._data["creator_identifier"] = creator_identifier

        # Pop from source data all keys that are defined in `_data` before
        #   this moment and through their values away
        # - they should be the same and if are not then should not change
        #   already set values
        for key in self._data.keys():
            if key in data:
                data.pop(key)

        self._data["variant"] = self._data.get("variant") or ""
        # Stored creator specific attribute values
        # {key: value}
        creator_values = copy.deepcopy(orig_creator_attributes)

        self._data["creator_attributes"] = creator_values

        # Stored publish specific attribute values
        # {<plugin name>: {key: value}}
        self._data["publish_attributes"] = PublishAttributes(
            self, orig_publish_attributes
        )
        if data:
            self._data.update(data)

        for key, default in self.__required_keys.items():
            self._data.setdefault(key, default)

        if not self._data.get("instance_id"):
            self._data["instance_id"] = str(uuid4())

        creator_attr_defs = creator.get_attr_defs_for_instance(self)
        self.set_create_attr_defs(
            creator_attr_defs, creator_values
        )

    def __str__(self):
        return (
            "<CreatedInstance {product[name]}"
            " ({product[type]}[{creator_identifier}])> {data}"
        ).format(
            creator_identifier=self.creator_identifier,
            product={"name": self.product_name, "type": self.product_type},
            data=str(self._data)
        )

    # --- Dictionary like methods ---
    def __getitem__(self, key):
        return self._data[key]

    def __contains__(self, key):
        return key in self._data

    def __setitem__(self, key, value):
        # Validate immutable keys
        if key in self.__immutable_keys:
            if value == self._data.get(key):
                return
            # Raise exception if key is immutable and value has changed
            raise ImmutableKeyError(key)

        if key in self._data and self._data[key] == value:
            return

        self._data[key] = value
        self._create_context.instance_values_changed(
            self.id, {key: value}
        )

    def get(self, key, default=None):
        return self._data.get(key, default)

    def pop(self, key, *args, **kwargs):
        # Raise exception if is trying to pop key which is immutable
        if key in self.__immutable_keys:
            raise ImmutableKeyError(key)

        has_key = key in self._data
        output = self._data.pop(key, *args, **kwargs)
        if has_key:
            if key in self.__required_keys:
                self._data[key] = self.__required_keys[key]
            self._create_context.instance_values_changed(
                self.id, {key: None}
            )
        return output

    def keys(self):
        return self._data.keys()

    def values(self):
        return self._data.values()

    def items(self):
        return self._data.items()
    # ------

    @property
    def product_type(self):
        return self._data["productType"]

    @property
    def product_name(self):
        return self._data["productName"]

    @property
    def label(self):
        label = self._data.get("label")
        if not label:
            label = self.product_name
        return label

    @property
    def group_label(self):
        label = self._data.get("group")
        if label:
            return label
        return self._group_label

    @property
    def origin_data(self):
        output = copy.deepcopy(self._orig_data)
        output["creator_attributes"] = self.creator_attributes.origin_data
        output["publish_attributes"] = self.publish_attributes.origin_data
        return output

    @property
    def creator_identifier(self):
        return self._data["creator_identifier"]

    @property
    def creator_label(self):
        return self._creator.label or self.creator_identifier

    @property
    def id(self):
        """Instance identifier.

        Returns:
            str: UUID of instance.
        """

        return self._data["instance_id"]

    @property
    def data(self):
        """Legacy access to data.

        Access to data is needed to modify values.

        Returns:
            CreatedInstance: Object can be used as dictionary but with
                validations of immutable keys.
        """

        return self

    @property
    def transient_data(self):
        """Data stored for lifetime of instance object.

        These data are not stored to scene and will be lost on object
        deletion.

        Can be used to store objects. In some host implementations is not
        possible to reference to object in scene with some unique identifier
        (e.g. node in Fusion.). In that case it is handy to store the object
        here. Should be used that way only if instance data are stored on the
        node itself.

        Returns:
            Dict[str, Any]: Dictionary object where you can store data related
                to instance for lifetime of instance object.
        """

        return self._transient_data

    def changes(self):
        """Calculate and return changes."""

        return TrackChangesItem(self.origin_data, self.data_to_store())

    def mark_as_stored(self):
        """Should be called when instance data are stored.

        Origin data are replaced by current data so changes are cleared.
        """

        orig_keys = set(self._orig_data.keys())
        for key, value in self._data.items():
            orig_keys.discard(key)
            if key in ("creator_attributes", "publish_attributes"):
                continue
            self._orig_data[key] = copy.deepcopy(value)

        for key in orig_keys:
            self._orig_data.pop(key)

        self.creator_attributes.mark_as_stored()
        self.publish_attributes.mark_as_stored()

    @property
    def creator_attributes(self):
        return self._data["creator_attributes"]

    @property
    def creator_attribute_defs(self):
        """Attribute definitions defined by creator plugin.

        Returns:
              List[AbstractAttrDef]: Attribute definitions.
        """

        return self.creator_attributes.attr_defs

    @property
    def publish_attributes(self):
        return self._data["publish_attributes"]

    @property
    def has_promised_context(self) -> bool:
        """Get context data that are promised to be set by creator.

        Returns:
            bool: Has context that won't bo validated. Artist can't change
                value when set to True.

        """
        return self._transient_data.get("has_promised_context", False)

    def data_to_store(self):
        """Collect data that contain json parsable types.

        It is possible to recreate the instance using these data.

        Todos:
            We probably don't need OrderedDict. When data are loaded they
                are not ordered anymore.

        Returns:
            OrderedDict: Ordered dictionary with instance data.
        """

        output = collections.OrderedDict()
        for key, value in self._data.items():
            if key in ("creator_attributes", "publish_attributes"):
                continue
            output[key] = value

        if isinstance(self.creator_attributes, AttributeValues):
            creator_attributes = self.creator_attributes.data_to_store()
        else:
            creator_attributes = copy.deepcopy(self.creator_attributes)
        output["creator_attributes"] = creator_attributes
        output["publish_attributes"] = self.publish_attributes.data_to_store()

        return output

    def set_create_attr_defs(self, attr_defs, value=None):
        """Create plugin updates create attribute definitions.

        Method called by create plugin when attribute definitions should
            be changed.

        Args:
            attr_defs (List[AbstractAttrDef]): Attribute definitions.
            value (Optional[Dict[str, Any]]): Values of attribute definitions.
                Current values are used if not passed in.

        """
        if value is None:
            value = self._data["creator_attributes"]

        if isinstance(value, AttributeValues):
            value = value.data_to_store()

        if isinstance(self._data["creator_attributes"], AttributeValues):
            origin_data = self._data["creator_attributes"].origin_data
        else:
            origin_data = copy.deepcopy(self._data["creator_attributes"])
        self._data["creator_attributes"] = CreatorAttributeValues(
            self,
            "creator_attributes",
            attr_defs,
            value,
            origin_data
        )
        self._create_context.instance_create_attr_defs_changed(self.id)

    @classmethod
    def from_existing(
        cls,
        instance_data: Dict[str, Any],
        creator: "BaseCreator",
        transient_data: Optional[Dict[str, Any]] = None,
    ) -> "CreatedInstance":
        """Convert instance data from workfile to CreatedInstance.

        Args:
            instance_data (Dict[str, Any]): Data in a structure ready for
                'CreatedInstance' object.
            creator (BaseCreator): Creator plugin which is creating the
                instance of for which the instance belongs.
            transient_data (Optional[dict[str, Any]]): Instance transient
                data.

        Returns:
            CreatedInstance: Instance object.

        """
        instance_data = copy.deepcopy(instance_data)

        product_type = instance_data.get("productType")
        if product_type is None:
            product_type = instance_data.get("family")
            if product_type is None:
                product_type = creator.product_type
        product_name = instance_data.get("productName")
        if product_name is None:
            product_name = instance_data.get("subset")

        return cls(
            product_type,
            product_name,
            instance_data,
            creator,
            transient_data=transient_data,
        )

    def attribute_value_changed(self, key, changes):
        """A value changed.

        Args:
            key (str): Key of attribute values.
            changes (Dict[str, Any]): Changes in values.

        """
        self._create_context.instance_values_changed(self.id, {key: changes})

    def set_publish_plugin_attr_defs(self, plugin_name, attr_defs):
        """Set attribute definitions for publish plugin.

        Args:
            plugin_name(str): Name of publish plugin.
            attr_defs(List[AbstractAttrDef]): Attribute definitions.

        """
        self.publish_attributes.set_publish_plugin_attr_defs(
            plugin_name, attr_defs
        )
        self._create_context.instance_publish_attr_defs_changed(
            self.id, plugin_name
        )

    def publish_attribute_value_changed(self, plugin_name, value):
        """Method called from PublishAttributes.

        Args:
            plugin_name (str): Plugin name.
            value (Dict[str, Any]): Changes in values for the plugin.

        """
        self._create_context.instance_values_changed(
            self.id,
            {
                "publish_attributes": {
                    plugin_name: value,
                },
            },
        )

    def add_members(self, members):
        """Currently unused method."""

        for member in members:
            if member not in self._members:
                self._members.append(member)

    @property
    def _create_context(self):
        """Get create context.

        Returns:
            CreateContext: Context object which wraps object.

        """
        return self._creator.create_context

creator_attribute_defs property

Attribute definitions defined by creator plugin.

Returns:

Type Description

List[AbstractAttrDef]: Attribute definitions.

data property

Legacy access to data.

Access to data is needed to modify values.

Returns:

Name Type Description
CreatedInstance

Object can be used as dictionary but with validations of immutable keys.

has_promised_context property

Get context data that are promised to be set by creator.

Returns:

Name Type Description
bool bool

Has context that won't bo validated. Artist can't change value when set to True.

id property

Instance identifier.

Returns:

Name Type Description
str

UUID of instance.

transient_data property

Data stored for lifetime of instance object.

These data are not stored to scene and will be lost on object deletion.

Can be used to store objects. In some host implementations is not possible to reference to object in scene with some unique identifier (e.g. node in Fusion.). In that case it is handy to store the object here. Should be used that way only if instance data are stored on the node itself.

Returns:

Type Description

Dict[str, Any]: Dictionary object where you can store data related to instance for lifetime of instance object.

add_members(members)

Currently unused method.

Source code in client/ayon_core/pipeline/create/structures.py
878
879
880
881
882
883
def add_members(self, members):
    """Currently unused method."""

    for member in members:
        if member not in self._members:
            self._members.append(member)

attribute_value_changed(key, changes)

A value changed.

Parameters:

Name Type Description Default
key str

Key of attribute values.

required
changes Dict[str, Any]

Changes in values.

required
Source code in client/ayon_core/pipeline/create/structures.py
836
837
838
839
840
841
842
843
844
def attribute_value_changed(self, key, changes):
    """A value changed.

    Args:
        key (str): Key of attribute values.
        changes (Dict[str, Any]): Changes in values.

    """
    self._create_context.instance_values_changed(self.id, {key: changes})

changes()

Calculate and return changes.

Source code in client/ayon_core/pipeline/create/structures.py
684
685
686
687
def changes(self):
    """Calculate and return changes."""

    return TrackChangesItem(self.origin_data, self.data_to_store())

data_to_store()

Collect data that contain json parsable types.

It is possible to recreate the instance using these data.

Todos

We probably don't need OrderedDict. When data are loaded they are not ordered anymore.

Returns:

Name Type Description
OrderedDict

Ordered dictionary with instance data.

Source code in client/ayon_core/pipeline/create/structures.py
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
def data_to_store(self):
    """Collect data that contain json parsable types.

    It is possible to recreate the instance using these data.

    Todos:
        We probably don't need OrderedDict. When data are loaded they
            are not ordered anymore.

    Returns:
        OrderedDict: Ordered dictionary with instance data.
    """

    output = collections.OrderedDict()
    for key, value in self._data.items():
        if key in ("creator_attributes", "publish_attributes"):
            continue
        output[key] = value

    if isinstance(self.creator_attributes, AttributeValues):
        creator_attributes = self.creator_attributes.data_to_store()
    else:
        creator_attributes = copy.deepcopy(self.creator_attributes)
    output["creator_attributes"] = creator_attributes
    output["publish_attributes"] = self.publish_attributes.data_to_store()

    return output

from_existing(instance_data, creator, transient_data=None) classmethod

Convert instance data from workfile to CreatedInstance.

Parameters:

Name Type Description Default
instance_data Dict[str, Any]

Data in a structure ready for 'CreatedInstance' object.

required
creator BaseCreator

Creator plugin which is creating the instance of for which the instance belongs.

required
transient_data Optional[dict[str, Any]]

Instance transient data.

None

Returns:

Name Type Description
CreatedInstance CreatedInstance

Instance object.

Source code in client/ayon_core/pipeline/create/structures.py
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
@classmethod
def from_existing(
    cls,
    instance_data: Dict[str, Any],
    creator: "BaseCreator",
    transient_data: Optional[Dict[str, Any]] = None,
) -> "CreatedInstance":
    """Convert instance data from workfile to CreatedInstance.

    Args:
        instance_data (Dict[str, Any]): Data in a structure ready for
            'CreatedInstance' object.
        creator (BaseCreator): Creator plugin which is creating the
            instance of for which the instance belongs.
        transient_data (Optional[dict[str, Any]]): Instance transient
            data.

    Returns:
        CreatedInstance: Instance object.

    """
    instance_data = copy.deepcopy(instance_data)

    product_type = instance_data.get("productType")
    if product_type is None:
        product_type = instance_data.get("family")
        if product_type is None:
            product_type = creator.product_type
    product_name = instance_data.get("productName")
    if product_name is None:
        product_name = instance_data.get("subset")

    return cls(
        product_type,
        product_name,
        instance_data,
        creator,
        transient_data=transient_data,
    )

mark_as_stored()

Should be called when instance data are stored.

Origin data are replaced by current data so changes are cleared.

Source code in client/ayon_core/pipeline/create/structures.py
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
def mark_as_stored(self):
    """Should be called when instance data are stored.

    Origin data are replaced by current data so changes are cleared.
    """

    orig_keys = set(self._orig_data.keys())
    for key, value in self._data.items():
        orig_keys.discard(key)
        if key in ("creator_attributes", "publish_attributes"):
            continue
        self._orig_data[key] = copy.deepcopy(value)

    for key in orig_keys:
        self._orig_data.pop(key)

    self.creator_attributes.mark_as_stored()
    self.publish_attributes.mark_as_stored()

publish_attribute_value_changed(plugin_name, value)

Method called from PublishAttributes.

Parameters:

Name Type Description Default
plugin_name str

Plugin name.

required
value Dict[str, Any]

Changes in values for the plugin.

required
Source code in client/ayon_core/pipeline/create/structures.py
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
def publish_attribute_value_changed(self, plugin_name, value):
    """Method called from PublishAttributes.

    Args:
        plugin_name (str): Plugin name.
        value (Dict[str, Any]): Changes in values for the plugin.

    """
    self._create_context.instance_values_changed(
        self.id,
        {
            "publish_attributes": {
                plugin_name: value,
            },
        },
    )

set_create_attr_defs(attr_defs, value=None)

Create plugin updates create attribute definitions.

Method called by create plugin when attribute definitions should be changed.

Parameters:

Name Type Description Default
attr_defs List[AbstractAttrDef]

Attribute definitions.

required
value Optional[Dict[str, Any]]

Values of attribute definitions. Current values are used if not passed in.

None
Source code in client/ayon_core/pipeline/create/structures.py
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
def set_create_attr_defs(self, attr_defs, value=None):
    """Create plugin updates create attribute definitions.

    Method called by create plugin when attribute definitions should
        be changed.

    Args:
        attr_defs (List[AbstractAttrDef]): Attribute definitions.
        value (Optional[Dict[str, Any]]): Values of attribute definitions.
            Current values are used if not passed in.

    """
    if value is None:
        value = self._data["creator_attributes"]

    if isinstance(value, AttributeValues):
        value = value.data_to_store()

    if isinstance(self._data["creator_attributes"], AttributeValues):
        origin_data = self._data["creator_attributes"].origin_data
    else:
        origin_data = copy.deepcopy(self._data["creator_attributes"])
    self._data["creator_attributes"] = CreatorAttributeValues(
        self,
        "creator_attributes",
        attr_defs,
        value,
        origin_data
    )
    self._create_context.instance_create_attr_defs_changed(self.id)

set_publish_plugin_attr_defs(plugin_name, attr_defs)

Set attribute definitions for publish plugin.

Parameters:

Name Type Description Default
plugin_name(str)

Name of publish plugin.

required
attr_defs(List[AbstractAttrDef])

Attribute definitions.

required
Source code in client/ayon_core/pipeline/create/structures.py
846
847
848
849
850
851
852
853
854
855
856
857
858
859
def set_publish_plugin_attr_defs(self, plugin_name, attr_defs):
    """Set attribute definitions for publish plugin.

    Args:
        plugin_name(str): Name of publish plugin.
        attr_defs(List[AbstractAttrDef]): Attribute definitions.

    """
    self.publish_attributes.set_publish_plugin_attr_defs(
        plugin_name, attr_defs
    )
    self._create_context.instance_publish_attr_defs_changed(
        self.id, plugin_name
    )

Creator

Bases: BaseCreator

Creator that has more information for artist to show in UI.

Creation requires prepared product name and instance data.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
class Creator(BaseCreator):
    """Creator that has more information for artist to show in UI.

    Creation requires prepared product name and instance data.
    """

    # GUI Purposes
    # - default_variants may not be used if `get_default_variants`
    #   is overridden
    default_variants = []

    # Default variant used in 'get_default_variant'
    _default_variant = None

    # Short description of product type
    # - may not be used if `get_description` is overridden
    description = None

    # Detailed description of product type for artists
    # - may not be used if `get_detail_description` is overridden
    detailed_description = None

    # It does make sense to change context on creation
    # - in some cases it may confuse artists because it would not be used
    #      e.g. for buld creators
    create_allow_context_change = True
    # A thumbnail can be passed in precreate attributes
    # - if is set to True is should expect that a thumbnail path under key
    #   PRE_CREATE_THUMBNAIL_KEY can be sent in data with precreate data
    # - is disabled by default because the feature was added in later stages
    #   and creators who would not expect PRE_CREATE_THUMBNAIL_KEY could
    #   cause issues with instance data
    create_allow_thumbnail = False

    # Precreate attribute definitions showed before creation
    # - similar to instance attribute definitions
    pre_create_attr_defs = []

    def __init__(self, *args, **kwargs):
        cls = self.__class__

        # Fix backwards compatibility for plugins which override
        #   'default_variant' attribute directly
        if not isinstance(cls.default_variant, property):
            # Move value from 'default_variant' to '_default_variant'
            self._default_variant = self.default_variant
            # Create property 'default_variant' on the class
            cls.default_variant = property(
                cls._get_default_variant_wrap,
                cls._set_default_variant_wrap
            )
        super().__init__(*args, **kwargs)

    @property
    def show_order(self):
        """Order in which is creator shown in UI.

        Returns:
            int: Order in which is creator shown (less == earlier). By default
                is using Creator's 'order' or processing.
        """

        return self.order

    @abstractmethod
    def create(self, product_name, instance_data, pre_create_data):
        """Create new instance and store it.

        Ideally should be stored to workfile using host implementation.

        Args:
            product_name(str): Product name of created instance.
            instance_data(dict): Base data for instance.
            pre_create_data(dict): Data based on pre creation attributes.
                Those may affect how creator works.
        """

        # instance = CreatedInstance(
        #     self.product_type, product_name, instance_data
        # )
        pass

    def get_description(self):
        """Short description of product type and plugin.

        Returns:
            str: Short description of product type.
        """

        return self.description

    def get_detail_description(self):
        """Description of product type and plugin.

        Can be detailed with markdown or html tags.

        Returns:
            str: Detailed description of product type for artist.
        """

        return self.detailed_description

    def get_default_variants(self):
        """Default variant values for UI tooltips.

        Replacement of `default_variants` attribute. Using method gives
        ability to have some "logic" other than attribute values.

        By default, returns `default_variants` value.

        Returns:
            list[str]: Whisper variants for user input.
        """

        return copy.deepcopy(self.default_variants)

    def get_default_variant(self, only_explicit=False):
        """Default variant value that will be used to prefill variant input.

        This is for user input and value may not be content of result from
        `get_default_variants`.

        Note:
            This method does not allow to have empty string as
                default variant.

        Args:
            only_explicit (Optional[bool]): If True, only explicit default
                variant from '_default_variant' will be returned.

        Returns:
            str: Variant value.
        """

        if only_explicit or self._default_variant:
            return self._default_variant

        for variant in self.get_default_variants():
            return variant
        return DEFAULT_VARIANT_VALUE

    def _get_default_variant_wrap(self):
        """Default variant value that will be used to prefill variant input.

        Wrapper for 'get_default_variant'.

        Notes:
            This method is wrapper for 'get_default_variant'
                for 'default_variant' property, so creator can override
                the method.

        Returns:
            str: Variant value.
        """

        return self.get_default_variant()

    def _set_default_variant_wrap(self, variant):
        """Set default variant value.

        This method is needed for automated settings overrides which are
        changing attributes based on keys in settings.

        Args:
            variant (str): New default variant value.
        """

        self._default_variant = variant

    default_variant = property(
        _get_default_variant_wrap,
        _set_default_variant_wrap
    )

    def get_pre_create_attr_defs(self):
        """Plugin attribute definitions needed for creation.
        Attribute definitions of plugin that define how creation will work.
        Values of these definitions are passed to `create` method.

        Note:
            Convert method should be implemented which should care about
            updating keys/values when plugin attributes change.

        Returns:
            list[AbstractAttrDef]: Attribute definitions that can be tweaked
                for created instance.
        """
        return self.pre_create_attr_defs

    def get_staging_dir(self, instance) -> Optional[StagingDir]:
        """Return the staging dir and persistence from instance.

        Args:
            instance (CreatedInstance): Instance for which should be staging
                dir gathered.

        Returns:
            Optional[namedtuple]: Staging dir path and persistence or None
        """
        create_ctx = self.create_context
        product_name = instance.get("productName")
        product_type = instance.get("productType")
        folder_path = instance.get("folderPath")

        # this can only work if product name and folder path are available
        if not product_name or not folder_path:
            return None

        publish_settings = self.project_settings["core"]["publish"]
        follow_workfile_version = (
            publish_settings
            ["CollectAnatomyInstanceData"]
            ["follow_workfile_version"]
        )
        follow_version_hosts = (
            publish_settings
            ["CollectSceneVersion"]
            ["hosts"]
        )

        current_host = create_ctx.host.name
        follow_workfile_version = (
            follow_workfile_version and
            current_host in follow_version_hosts
        )

        # Gather version number provided from the instance.
        current_workfile = create_ctx.get_current_workfile_path()
        version = instance.get("version")

        # If follow workfile, gather version from workfile path.
        if version is None and follow_workfile_version and current_workfile:
            workfile_version = get_version_from_path(current_workfile)
            if workfile_version is not None:
                version = int(workfile_version)

        # Fill-up version with next version available.
        if version is None:
            versions = self.get_next_versions_for_instances(
                [instance]
            )
            version, = tuple(versions.values())

        template_data = {"version": version}

        staging_dir_info = get_staging_dir_info(
            create_ctx.get_current_project_entity(),
            create_ctx.get_folder_entity(folder_path),
            create_ctx.get_task_entity(folder_path, instance.get("task")),
            product_type,
            product_name,
            create_ctx.host_name,
            anatomy=create_ctx.get_current_project_anatomy(),
            project_settings=create_ctx.get_current_project_settings(),
            always_return_path=False,
            logger=self.log,
            template_data=template_data,
        )

        return staging_dir_info or None

    def apply_staging_dir(self, instance):
        """Apply staging dir with persistence to instance's transient data.

        Method is called on instance creation and on instance update.

        Args:
            instance (CreatedInstance): Instance for which should be staging
                dir applied.

        Returns:
            Optional[str]: Staging dir path or None if not applied.
        """
        staging_dir_info = self.get_staging_dir(instance)
        if staging_dir_info is None:
            return None

        # path might be already created by get_staging_dir_info
        staging_dir_path = staging_dir_info.directory
        os.makedirs(staging_dir_path, exist_ok=True)

        instance.transient_data.update({
            "stagingDir": staging_dir_path,
            "stagingDir_persistent": staging_dir_info.is_persistent,
            "stagingDir_is_custom": staging_dir_info.is_custom,
        })

        self.log.info(f"Applied staging dir to instance: {staging_dir_path}")

        return staging_dir_path

    def _pre_create_attr_defs_changed(self):
        """Called when pre-create attribute definitions change.

        Create plugin can call this method when knows that
            'get_pre_create_attr_defs' should be called again.
        """
        self.create_context.create_plugin_pre_create_attr_defs_changed(
            self.identifier
        )

show_order property

Order in which is creator shown in UI.

Returns:

Name Type Description
int

Order in which is creator shown (less == earlier). By default is using Creator's 'order' or processing.

apply_staging_dir(instance)

Apply staging dir with persistence to instance's transient data.

Method is called on instance creation and on instance update.

Parameters:

Name Type Description Default
instance CreatedInstance

Instance for which should be staging dir applied.

required

Returns:

Type Description

Optional[str]: Staging dir path or None if not applied.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
def apply_staging_dir(self, instance):
    """Apply staging dir with persistence to instance's transient data.

    Method is called on instance creation and on instance update.

    Args:
        instance (CreatedInstance): Instance for which should be staging
            dir applied.

    Returns:
        Optional[str]: Staging dir path or None if not applied.
    """
    staging_dir_info = self.get_staging_dir(instance)
    if staging_dir_info is None:
        return None

    # path might be already created by get_staging_dir_info
    staging_dir_path = staging_dir_info.directory
    os.makedirs(staging_dir_path, exist_ok=True)

    instance.transient_data.update({
        "stagingDir": staging_dir_path,
        "stagingDir_persistent": staging_dir_info.is_persistent,
        "stagingDir_is_custom": staging_dir_info.is_custom,
    })

    self.log.info(f"Applied staging dir to instance: {staging_dir_path}")

    return staging_dir_path

create(product_name, instance_data, pre_create_data) abstractmethod

Create new instance and store it.

Ideally should be stored to workfile using host implementation.

Parameters:

Name Type Description Default
product_name(str)

Product name of created instance.

required
instance_data(dict)

Base data for instance.

required
pre_create_data(dict)

Data based on pre creation attributes. Those may affect how creator works.

required
Source code in client/ayon_core/pipeline/create/creator_plugins.py
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
@abstractmethod
def create(self, product_name, instance_data, pre_create_data):
    """Create new instance and store it.

    Ideally should be stored to workfile using host implementation.

    Args:
        product_name(str): Product name of created instance.
        instance_data(dict): Base data for instance.
        pre_create_data(dict): Data based on pre creation attributes.
            Those may affect how creator works.
    """

    # instance = CreatedInstance(
    #     self.product_type, product_name, instance_data
    # )
    pass

get_default_variant(only_explicit=False)

Default variant value that will be used to prefill variant input.

This is for user input and value may not be content of result from get_default_variants.

Note

This method does not allow to have empty string as default variant.

Parameters:

Name Type Description Default
only_explicit Optional[bool]

If True, only explicit default variant from '_default_variant' will be returned.

False

Returns:

Name Type Description
str

Variant value.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
def get_default_variant(self, only_explicit=False):
    """Default variant value that will be used to prefill variant input.

    This is for user input and value may not be content of result from
    `get_default_variants`.

    Note:
        This method does not allow to have empty string as
            default variant.

    Args:
        only_explicit (Optional[bool]): If True, only explicit default
            variant from '_default_variant' will be returned.

    Returns:
        str: Variant value.
    """

    if only_explicit or self._default_variant:
        return self._default_variant

    for variant in self.get_default_variants():
        return variant
    return DEFAULT_VARIANT_VALUE

get_default_variants()

Default variant values for UI tooltips.

Replacement of default_variants attribute. Using method gives ability to have some "logic" other than attribute values.

By default, returns default_variants value.

Returns:

Type Description

list[str]: Whisper variants for user input.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
753
754
755
756
757
758
759
760
761
762
763
764
765
def get_default_variants(self):
    """Default variant values for UI tooltips.

    Replacement of `default_variants` attribute. Using method gives
    ability to have some "logic" other than attribute values.

    By default, returns `default_variants` value.

    Returns:
        list[str]: Whisper variants for user input.
    """

    return copy.deepcopy(self.default_variants)

get_description()

Short description of product type and plugin.

Returns:

Name Type Description
str

Short description of product type.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
733
734
735
736
737
738
739
740
def get_description(self):
    """Short description of product type and plugin.

    Returns:
        str: Short description of product type.
    """

    return self.description

get_detail_description()

Description of product type and plugin.

Can be detailed with markdown or html tags.

Returns:

Name Type Description
str

Detailed description of product type for artist.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
742
743
744
745
746
747
748
749
750
751
def get_detail_description(self):
    """Description of product type and plugin.

    Can be detailed with markdown or html tags.

    Returns:
        str: Detailed description of product type for artist.
    """

    return self.detailed_description

get_pre_create_attr_defs()

Plugin attribute definitions needed for creation. Attribute definitions of plugin that define how creation will work. Values of these definitions are passed to create method.

Note

Convert method should be implemented which should care about updating keys/values when plugin attributes change.

Returns:

Type Description

list[AbstractAttrDef]: Attribute definitions that can be tweaked for created instance.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
825
826
827
828
829
830
831
832
833
834
835
836
837
838
def get_pre_create_attr_defs(self):
    """Plugin attribute definitions needed for creation.
    Attribute definitions of plugin that define how creation will work.
    Values of these definitions are passed to `create` method.

    Note:
        Convert method should be implemented which should care about
        updating keys/values when plugin attributes change.

    Returns:
        list[AbstractAttrDef]: Attribute definitions that can be tweaked
            for created instance.
    """
    return self.pre_create_attr_defs

get_staging_dir(instance)

Return the staging dir and persistence from instance.

Parameters:

Name Type Description Default
instance CreatedInstance

Instance for which should be staging dir gathered.

required

Returns:

Type Description
Optional[StagingDir]

Optional[namedtuple]: Staging dir path and persistence or None

Source code in client/ayon_core/pipeline/create/creator_plugins.py
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
def get_staging_dir(self, instance) -> Optional[StagingDir]:
    """Return the staging dir and persistence from instance.

    Args:
        instance (CreatedInstance): Instance for which should be staging
            dir gathered.

    Returns:
        Optional[namedtuple]: Staging dir path and persistence or None
    """
    create_ctx = self.create_context
    product_name = instance.get("productName")
    product_type = instance.get("productType")
    folder_path = instance.get("folderPath")

    # this can only work if product name and folder path are available
    if not product_name or not folder_path:
        return None

    publish_settings = self.project_settings["core"]["publish"]
    follow_workfile_version = (
        publish_settings
        ["CollectAnatomyInstanceData"]
        ["follow_workfile_version"]
    )
    follow_version_hosts = (
        publish_settings
        ["CollectSceneVersion"]
        ["hosts"]
    )

    current_host = create_ctx.host.name
    follow_workfile_version = (
        follow_workfile_version and
        current_host in follow_version_hosts
    )

    # Gather version number provided from the instance.
    current_workfile = create_ctx.get_current_workfile_path()
    version = instance.get("version")

    # If follow workfile, gather version from workfile path.
    if version is None and follow_workfile_version and current_workfile:
        workfile_version = get_version_from_path(current_workfile)
        if workfile_version is not None:
            version = int(workfile_version)

    # Fill-up version with next version available.
    if version is None:
        versions = self.get_next_versions_for_instances(
            [instance]
        )
        version, = tuple(versions.values())

    template_data = {"version": version}

    staging_dir_info = get_staging_dir_info(
        create_ctx.get_current_project_entity(),
        create_ctx.get_folder_entity(folder_path),
        create_ctx.get_task_entity(folder_path, instance.get("task")),
        product_type,
        product_name,
        create_ctx.host_name,
        anatomy=create_ctx.get_current_project_anatomy(),
        project_settings=create_ctx.get_current_project_settings(),
        always_return_path=False,
        logger=self.log,
        template_data=template_data,
    )

    return staging_dir_info or None

CreatorAttributeValues

Bases: AttributeValues

Creator specific attribute values of an instance.

Source code in client/ayon_core/pipeline/create/structures.py
232
233
234
235
236
237
class CreatorAttributeValues(AttributeValues):
    """Creator specific attribute values of an instance."""

    @property
    def instance(self):
        return self._parent

CreatorError

Bases: Exception

Should be raised when creator failed because of known issue.

Message of error should be artist friendly.

Source code in client/ayon_core/pipeline/create/exceptions.py
63
64
65
66
67
68
class CreatorError(Exception):
    """Should be raised when creator failed because of known issue.

    Message of error should be artist friendly.
    """
    pass

CreatorsOperationFailed

Bases: Exception

Raised when a creator process crashes in 'CreateContext'.

The exception contains information about the creator and error. The data are prepared using 'prepare_failed_creator_operation_info' and can be serialized using json.

Usage is for UI purposes which may not have access to exceptions directly and would not have ability to catch exceptions 'per creator'.

Parameters:

Name Type Description Default
msg str

General error message.

required
failed_info list[dict[str, Any]]

List of failed creators with exception message and optionally formatted traceback.

required
Source code in client/ayon_core/pipeline/create/exceptions.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class CreatorsOperationFailed(Exception):
    """Raised when a creator process crashes in 'CreateContext'.

    The exception contains information about the creator and error. The data
    are prepared using 'prepare_failed_creator_operation_info' and can be
    serialized using json.

    Usage is for UI purposes which may not have access to exceptions directly
    and would not have ability to catch exceptions 'per creator'.

    Args:
        msg (str): General error message.
        failed_info (list[dict[str, Any]]): List of failed creators with
            exception message and optionally formatted traceback.
    """

    def __init__(self, msg, failed_info):
        super().__init__(msg)
        self.failed_info = failed_info

HostMissRequiredMethod

Bases: Exception

Host does not have implemented required functions for creation.

Source code in client/ayon_core/pipeline/create/exceptions.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class HostMissRequiredMethod(Exception):
    """Host does not have implemented required functions for creation."""

    def __init__(self, host, missing_methods):
        self.missing_methods = missing_methods
        self.host = host
        joined_methods = ", ".join(
            ['"{}"'.format(name) for name in missing_methods]
        )
        dirpath = os.path.dirname(
            os.path.normpath(inspect.getsourcefile(host))
        )
        dirpath_parts = dirpath.split(os.path.sep)
        host_name = dirpath_parts.pop(-1)
        if host_name == "api":
            host_name = dirpath_parts.pop(-1)

        msg = "Host \"{}\" does not have implemented method/s {}".format(
            host_name, joined_methods
        )
        super().__init__(msg)

ImmutableKeyError

Bases: TypeError

Accessed key is immutable so does not allow changes or removals.

Source code in client/ayon_core/pipeline/create/exceptions.py
10
11
12
13
14
15
16
17
18
19
class ImmutableKeyError(TypeError):
    """Accessed key is immutable so does not allow changes or removals."""

    def __init__(self, key, msg=None):
        self.immutable_key = key
        if not msg:
            msg = "Key \"{}\" is immutable and does not allow changes.".format(
                key
            )
        super().__init__(msg)

LegacyCreator

Determine how assets are created

Source code in client/ayon_core/pipeline/create/legacy_create.py
 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
class LegacyCreator:
    """Determine how assets are created"""
    label = None
    product_type = None
    defaults = None
    maintain_selection = True
    enabled = True

    dynamic_product_name_keys = []

    log = logging.getLogger("LegacyCreator")
    log.propagate = True

    def __init__(self, name, folder_path, options=None, data=None):
        self.name = name  # For backwards compatibility
        self.options = options

        # Default data
        self.data = collections.OrderedDict()
        # TODO use 'AYON_INSTANCE_ID' when all hosts support it
        self.data["id"] = AYON_INSTANCE_ID
        self.data["productType"] = self.product_type
        self.data["folderPath"] = folder_path
        self.data["productName"] = name
        self.data["active"] = True

        self.data.update(data or {})

    @classmethod
    def apply_settings(cls, project_settings):
        """Apply AYON settings to a plugin class."""

        host_name = os.environ.get("AYON_HOST_NAME")
        plugin_type = "create"
        plugin_type_settings = (
            project_settings
            .get(host_name, {})
            .get(plugin_type, {})
        )
        global_type_settings = (
            project_settings
            .get("core", {})
            .get(plugin_type, {})
        )
        if not global_type_settings and not plugin_type_settings:
            return

        plugin_name = cls.__name__

        plugin_settings = None
        # Look for plugin settings in host specific settings
        if plugin_name in plugin_type_settings:
            plugin_settings = plugin_type_settings[plugin_name]

        # Look for plugin settings in global settings
        elif plugin_name in global_type_settings:
            plugin_settings = global_type_settings[plugin_name]

        if not plugin_settings:
            return

        cls.log.debug(">>> We have preset for {}".format(plugin_name))
        for option, value in plugin_settings.items():
            if option == "enabled" and value is False:
                cls.log.debug("  - is disabled by preset")
            else:
                cls.log.debug("  - setting `{}`: `{}`".format(option, value))
            setattr(cls, option, value)

    def process(self):
        pass

    @classmethod
    def get_dynamic_data(
        cls, project_name, folder_entity, task_entity, variant, host_name
    ):
        """Return dynamic data for current Creator plugin.

        By default return keys from `dynamic_product_name_keys` attribute
        as mapping to keep formatted template unchanged.

        ```
        dynamic_product_name_keys = ["my_key"]
        ---
        output = {
            "my_key": "{my_key}"
        }
        ```

        Dynamic keys may override default Creator keys (productType, task,
        folderPath, ...) but do it wisely if you need.

        All of keys will be converted into 3 variants unchanged, capitalized
        and all upper letters. Because of that are all keys lowered.

        This method can be modified to prefill some values just keep in mind it
        is class method.

        Args:
            project_name (str): Context's project name.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            variant (str): What is entered by user in creator tool.
            host_name (str): Name of host.

        Returns:
            dict: Fill data for product name template.
        """
        dynamic_data = {}
        for key in cls.dynamic_product_name_keys:
            key = key.lower()
            dynamic_data[key] = "{" + key + "}"
        return dynamic_data

    @classmethod
    def get_product_name(
        cls, project_name, folder_entity, task_entity, variant, host_name=None
    ):
        """Return product name created with entered arguments.

        Logic extracted from Creator tool. This method should give ability
        to get product name without the tool.

        TODO: Maybe change `variant` variable.

        By default is output concatenated product type with variant.

        Args:
            project_name (str): Context's project name.
            folder_entity (dict[str, Any]): Folder entity.
            task_entity (dict[str, Any]): Task entity.
            variant (str): What is entered by user in creator tool.
            host_name (str): Name of host.

        Returns:
            str: Formatted product name with entered arguments. Should match
                config's logic.
        """

        dynamic_data = cls.get_dynamic_data(
            project_name, folder_entity, task_entity, variant, host_name
        )
        task_name = task_type = None
        if task_entity:
            task_name = task_entity["name"]
            task_type = task_entity["taskType"]
        return get_product_name(
            project_name,
            task_name,
            task_type,
            host_name,
            cls.product_type,
            variant,
            dynamic_data=dynamic_data
        )

apply_settings(project_settings) classmethod

Apply AYON settings to a plugin class.

Source code in client/ayon_core/pipeline/create/legacy_create.py
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
@classmethod
def apply_settings(cls, project_settings):
    """Apply AYON settings to a plugin class."""

    host_name = os.environ.get("AYON_HOST_NAME")
    plugin_type = "create"
    plugin_type_settings = (
        project_settings
        .get(host_name, {})
        .get(plugin_type, {})
    )
    global_type_settings = (
        project_settings
        .get("core", {})
        .get(plugin_type, {})
    )
    if not global_type_settings and not plugin_type_settings:
        return

    plugin_name = cls.__name__

    plugin_settings = None
    # Look for plugin settings in host specific settings
    if plugin_name in plugin_type_settings:
        plugin_settings = plugin_type_settings[plugin_name]

    # Look for plugin settings in global settings
    elif plugin_name in global_type_settings:
        plugin_settings = global_type_settings[plugin_name]

    if not plugin_settings:
        return

    cls.log.debug(">>> We have preset for {}".format(plugin_name))
    for option, value in plugin_settings.items():
        if option == "enabled" and value is False:
            cls.log.debug("  - is disabled by preset")
        else:
            cls.log.debug("  - setting `{}`: `{}`".format(option, value))
        setattr(cls, option, value)

get_dynamic_data(project_name, folder_entity, task_entity, variant, host_name) classmethod

Return dynamic data for current Creator plugin.

By default return keys from dynamic_product_name_keys attribute as mapping to keep formatted template unchanged.

dynamic_product_name_keys = ["my_key"]
---
output = {
    "my_key": "{my_key}"
}

Dynamic keys may override default Creator keys (productType, task, folderPath, ...) but do it wisely if you need.

All of keys will be converted into 3 variants unchanged, capitalized and all upper letters. Because of that are all keys lowered.

This method can be modified to prefill some values just keep in mind it is class method.

Parameters:

Name Type Description Default
project_name str

Context's project name.

required
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
variant str

What is entered by user in creator tool.

required
host_name str

Name of host.

required

Returns:

Name Type Description
dict

Fill data for product name template.

Source code in client/ayon_core/pipeline/create/legacy_create.py
 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
@classmethod
def get_dynamic_data(
    cls, project_name, folder_entity, task_entity, variant, host_name
):
    """Return dynamic data for current Creator plugin.

    By default return keys from `dynamic_product_name_keys` attribute
    as mapping to keep formatted template unchanged.

    ```
    dynamic_product_name_keys = ["my_key"]
    ---
    output = {
        "my_key": "{my_key}"
    }
    ```

    Dynamic keys may override default Creator keys (productType, task,
    folderPath, ...) but do it wisely if you need.

    All of keys will be converted into 3 variants unchanged, capitalized
    and all upper letters. Because of that are all keys lowered.

    This method can be modified to prefill some values just keep in mind it
    is class method.

    Args:
        project_name (str): Context's project name.
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        variant (str): What is entered by user in creator tool.
        host_name (str): Name of host.

    Returns:
        dict: Fill data for product name template.
    """
    dynamic_data = {}
    for key in cls.dynamic_product_name_keys:
        key = key.lower()
        dynamic_data[key] = "{" + key + "}"
    return dynamic_data

get_product_name(project_name, folder_entity, task_entity, variant, host_name=None) classmethod

Return product name created with entered arguments.

Logic extracted from Creator tool. This method should give ability to get product name without the tool.

TODO: Maybe change variant variable.

By default is output concatenated product type with variant.

Parameters:

Name Type Description Default
project_name str

Context's project name.

required
folder_entity dict[str, Any]

Folder entity.

required
task_entity dict[str, Any]

Task entity.

required
variant str

What is entered by user in creator tool.

required
host_name str

Name of host.

None

Returns:

Name Type Description
str

Formatted product name with entered arguments. Should match config's logic.

Source code in client/ayon_core/pipeline/create/legacy_create.py
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
@classmethod
def get_product_name(
    cls, project_name, folder_entity, task_entity, variant, host_name=None
):
    """Return product name created with entered arguments.

    Logic extracted from Creator tool. This method should give ability
    to get product name without the tool.

    TODO: Maybe change `variant` variable.

    By default is output concatenated product type with variant.

    Args:
        project_name (str): Context's project name.
        folder_entity (dict[str, Any]): Folder entity.
        task_entity (dict[str, Any]): Task entity.
        variant (str): What is entered by user in creator tool.
        host_name (str): Name of host.

    Returns:
        str: Formatted product name with entered arguments. Should match
            config's logic.
    """

    dynamic_data = cls.get_dynamic_data(
        project_name, folder_entity, task_entity, variant, host_name
    )
    task_name = task_type = None
    if task_entity:
        task_name = task_entity["name"]
        task_type = task_entity["taskType"]
    return get_product_name(
        project_name,
        task_name,
        task_type,
        host_name,
        cls.product_type,
        variant,
        dynamic_data=dynamic_data
    )

PublishAttributeValues

Bases: AttributeValues

Publish plugin specific attribute values.

Values are for single plugin which can be on CreatedInstance or context values stored on CreateContext.

Source code in client/ayon_core/pipeline/create/structures.py
240
241
242
243
244
245
246
247
248
249
class PublishAttributeValues(AttributeValues):
    """Publish plugin specific attribute values.

    Values are for single plugin which can be on `CreatedInstance`
    or context values stored on `CreateContext`.
    """

    @property
    def publish_attributes(self):
        return self._parent

PublishAttributes

Wrapper for publish plugin attribute definitions.

Cares about handling attribute definitions of multiple publish plugins. Keep information about attribute definitions and their values.

Parameters:

Name Type Description Default
parent(CreatedInstance, CreateContext

Parent for which will be data stored and from which are data loaded.

required
origin_data(dict)

Loaded data by plugin class name.

required
Source code in client/ayon_core/pipeline/create/structures.py
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
class PublishAttributes:
    """Wrapper for publish plugin attribute definitions.

    Cares about handling attribute definitions of multiple publish plugins.
    Keep information about attribute definitions and their values.

    Args:
        parent(CreatedInstance, CreateContext): Parent for which will be
            data stored and from which are data loaded.
        origin_data(dict): Loaded data by plugin class name.

    """
    def __init__(self, parent, origin_data):
        self._parent = parent
        self._origin_data = copy.deepcopy(origin_data)

        self._data = copy.deepcopy(origin_data)

    def __getitem__(self, key):
        return self._data[key]

    def __contains__(self, key):
        return key in self._data

    def keys(self):
        return self._data.keys()

    def values(self):
        return self._data.values()

    def items(self):
        return self._data.items()

    def get(self, key, default=None):
        return self._data.get(key, default)

    def pop(self, key, default=None):
        """Remove or reset value for plugin.

        Plugin values are reset to defaults if plugin is available but
        data of plugin which was not found are removed.

        Args:
            key(str): Plugin name.
            default: Default value if plugin was not found.
        """

        if key not in self._data:
            return default

        value = self._data[key]
        if not isinstance(value, AttributeValues):
            self.attribute_value_changed(key, None)
            return self._data.pop(key)

        value_item = self._data[key]
        # Prepare value to return
        output = value_item.data_to_store()
        # Reset values
        value_item.reset_values()
        self.attribute_value_changed(
            key, value_item.data_to_store()
        )
        return output

    def mark_as_stored(self):
        self._origin_data = copy.deepcopy(self.data_to_store())

    def data_to_store(self):
        """Convert attribute values to "data to store"."""
        output = {}
        for key, attr_value in self._data.items():
            if isinstance(attr_value, AttributeValues):
                output[key] = attr_value.data_to_store()
            else:
                output[key] = attr_value
        return output

    @property
    def origin_data(self):
        return copy.deepcopy(self._origin_data)

    def attribute_value_changed(self, key, changes):
        self._parent.publish_attribute_value_changed(key,  changes)

    def set_publish_plugin_attr_defs(
        self,
        plugin_name: str,
        attr_defs: List[AbstractAttrDef],
        value: Optional[Dict[str, Any]] = None
    ):
        """Set attribute definitions for plugin.

        Args:
            plugin_name (str): Name of plugin.
            attr_defs (List[AbstractAttrDef]): Attribute definitions.
            value (Optional[Dict[str, Any]]): Attribute values.

        """
        # TODO what if 'attr_defs' is 'None'?
        if value is None:
            value = self._data.get(plugin_name)

        if value is None:
            value = {}

        self._data[plugin_name] = PublishAttributeValues(
            self, plugin_name, attr_defs, value, value
        )

    def serialize_attributes(self):
        return {
            "attr_defs": {
                plugin_name: attrs_value.get_serialized_attr_defs()
                for plugin_name, attrs_value in self._data.items()
            },
        }

    def deserialize_attributes(self, data):
        attr_defs = deserialize_attr_defs(data["attr_defs"])

        origin_data = self._origin_data
        data = self._data
        self._data = {}

        added_keys = set()
        for plugin_name, attr_defs_data in attr_defs.items():
            attr_defs = deserialize_attr_defs(attr_defs_data)
            value = data.get(plugin_name) or {}
            orig_value = copy.deepcopy(origin_data.get(plugin_name) or {})
            self._data[plugin_name] = PublishAttributeValues(
                self, plugin_name, attr_defs, value, orig_value
            )

        for key, value in data.items():
            if key not in added_keys:
                self._data[key] = value

data_to_store()

Convert attribute values to "data to store".

Source code in client/ayon_core/pipeline/create/structures.py
320
321
322
323
324
325
326
327
328
def data_to_store(self):
    """Convert attribute values to "data to store"."""
    output = {}
    for key, attr_value in self._data.items():
        if isinstance(attr_value, AttributeValues):
            output[key] = attr_value.data_to_store()
        else:
            output[key] = attr_value
    return output

pop(key, default=None)

Remove or reset value for plugin.

Plugin values are reset to defaults if plugin is available but data of plugin which was not found are removed.

Parameters:

Name Type Description Default
key(str)

Plugin name.

required
default

Default value if plugin was not found.

None
Source code in client/ayon_core/pipeline/create/structures.py
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
def pop(self, key, default=None):
    """Remove or reset value for plugin.

    Plugin values are reset to defaults if plugin is available but
    data of plugin which was not found are removed.

    Args:
        key(str): Plugin name.
        default: Default value if plugin was not found.
    """

    if key not in self._data:
        return default

    value = self._data[key]
    if not isinstance(value, AttributeValues):
        self.attribute_value_changed(key, None)
        return self._data.pop(key)

    value_item = self._data[key]
    # Prepare value to return
    output = value_item.data_to_store()
    # Reset values
    value_item.reset_values()
    self.attribute_value_changed(
        key, value_item.data_to_store()
    )
    return output

set_publish_plugin_attr_defs(plugin_name, attr_defs, value=None)

Set attribute definitions for plugin.

Parameters:

Name Type Description Default
plugin_name str

Name of plugin.

required
attr_defs List[AbstractAttrDef]

Attribute definitions.

required
value Optional[Dict[str, Any]]

Attribute values.

None
Source code in client/ayon_core/pipeline/create/structures.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
def set_publish_plugin_attr_defs(
    self,
    plugin_name: str,
    attr_defs: List[AbstractAttrDef],
    value: Optional[Dict[str, Any]] = None
):
    """Set attribute definitions for plugin.

    Args:
        plugin_name (str): Name of plugin.
        attr_defs (List[AbstractAttrDef]): Attribute definitions.
        value (Optional[Dict[str, Any]]): Attribute values.

    """
    # TODO what if 'attr_defs' is 'None'?
    if value is None:
        value = self._data.get(plugin_name)

    if value is None:
        value = {}

    self._data[plugin_name] = PublishAttributeValues(
        self, plugin_name, attr_defs, value, value
    )

UnavailableSharedData

Bases: Exception

Shared data are not available at the moment when are accessed.

Source code in client/ayon_core/pipeline/create/exceptions.py
5
6
7
class UnavailableSharedData(Exception):
    """Shared data are not available at the moment when are accessed."""
    pass

cache_and_get_instances(creator, shared_key, list_instances_func)

Common approach to cache instances in shared data.

This is helper function which does not handle cases when a 'shared_key' is used for different list instances functions. The same approach of caching instances into 'collection_shared_data' is not required but is so common we've decided to unify it to some degree.

Function 'list_instances_func' is called only if 'shared_key' is not available in 'collection_shared_data' on creator.

Parameters:

Name Type Description Default
creator Creator

Plugin which would like to get instance data.

required
shared_key str

Key under which output of function will be stored.

required
list_instances_func Function

Function that will return instance data if data were not yet stored under 'shared_key'.

required

Returns:

Type Description

dict[str, dict[str, Any]]: Cached instances by creator identifier from result of passed function.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
def cache_and_get_instances(creator, shared_key, list_instances_func):
    """Common approach to cache instances in shared data.

    This is helper function which does not handle cases when a 'shared_key' is
    used for different list instances functions. The same approach of caching
    instances into 'collection_shared_data' is not required but is so common
    we've decided to unify it to some degree.

    Function 'list_instances_func' is called only if 'shared_key' is not
    available in 'collection_shared_data' on creator.

    Args:
        creator (Creator): Plugin which would like to get instance data.
        shared_key (str): Key under which output of function will be stored.
        list_instances_func (Function): Function that will return instance data
            if data were not yet stored under 'shared_key'.

    Returns:
        dict[str, dict[str, Any]]: Cached instances by creator identifier from
            result of passed function.
    """

    if shared_key not in creator.collection_shared_data:
        value = collections.defaultdict(list)
        for instance in list_instances_func():
            identifier = instance.get("creator_identifier")
            value[identifier].append(instance)
        creator.collection_shared_data[shared_key] = value
    return creator.collection_shared_data[shared_key]

get_last_versions_for_instances(project_name, instances, use_value_for_missing=False)

Get last versions for instances by their folder path and product name.

Parameters:

Name Type Description Default
project_name str

Project name.

required
instances list[CreatedInstance]

Instances to get next versions for.

required
use_value_for_missing Optional[bool]

Missing values are replaced with negative value if True. Otherwise None is used. -2 is used for instances without filled folder or product name. -1 is used for missing entities.

False

Returns:

Type Description

dict[str, Union[int, None]]: Last versions by instance id.

Source code in client/ayon_core/pipeline/create/utils.py
 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
def get_last_versions_for_instances(
    project_name, instances, use_value_for_missing=False
):
    """Get last versions for instances by their folder path and product name.

    Args:
        project_name (str): Project name.
        instances (list[CreatedInstance]): Instances to get next versions for.
        use_value_for_missing (Optional[bool]): Missing values are replaced
            with negative value if True. Otherwise None is used. -2 is used
            for instances without filled folder or product name. -1 is used
            for missing entities.

    Returns:
        dict[str, Union[int, None]]: Last versions by instance id.
    """

    output = {
        instance.id: -1 if use_value_for_missing else None
        for instance in instances
    }
    product_names_by_folder_path = collections.defaultdict(set)
    instances_by_hierarchy = {}
    for instance in instances:
        folder_path = instance.data.get("folderPath")
        product_name = instance.product_name
        if not folder_path or not product_name:
            if use_value_for_missing:
                output[instance.id] = -2
            continue

        (
            instances_by_hierarchy
            .setdefault(folder_path, {})
            .setdefault(product_name, [])
            .append(instance)
        )
        product_names_by_folder_path[folder_path].add(product_name)

    product_names = set()
    for names in product_names_by_folder_path.values():
        product_names |= names

    if not product_names:
        return output

    folder_entities = ayon_api.get_folders(
        project_name,
        folder_paths=product_names_by_folder_path.keys(),
        fields={"id", "path"}
    )
    folder_paths_by_id = {
        folder_entity["id"]: folder_entity["path"]
        for folder_entity in folder_entities
    }
    if not folder_paths_by_id:
        return output

    product_entities = ayon_api.get_products(
        project_name,
        folder_ids=folder_paths_by_id.keys(),
        product_names=product_names,
        fields={"id", "name", "folderId"}
    )
    product_entities_by_id = {}
    for product_entity in product_entities:
        # Filter product entities by names under parent
        folder_id = product_entity["folderId"]
        product_name = product_entity["name"]
        folder_path = folder_paths_by_id[folder_id]
        if product_name not in product_names_by_folder_path[folder_path]:
            continue
        product_entities_by_id[product_entity["id"]] = product_entity

    if not product_entities_by_id:
        return output

    last_versions_by_product_id = ayon_api.get_last_versions(
        project_name,
        product_entities_by_id.keys(),
        fields={"version", "productId"}
    )
    for product_id, version_entity in last_versions_by_product_id.items():
        product_entity = product_entities_by_id[product_id]
        product_name = product_entity["name"]
        folder_id = product_entity["folderId"]
        folder_path = folder_paths_by_id[folder_id]
        _instances = instances_by_hierarchy[folder_path][product_name]
        for instance in _instances:
            output[instance.id] = version_entity["version"]

    return output

get_legacy_creator_by_name(creator_name, case_sensitive=False)

Find creator plugin by name.

Parameters:

Name Type Description Default
creator_name str

Name of creator class that should be returned.

required
case_sensitive bool

Match of creator plugin name is case sensitive. Set to False by default.

False

Returns:

Name Type Description
Creator

Return first matching plugin or None.

Source code in client/ayon_core/pipeline/create/creator_plugins.py
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
def get_legacy_creator_by_name(creator_name, case_sensitive=False):
    """Find creator plugin by name.

    Args:
        creator_name (str): Name of creator class that should be returned.
        case_sensitive (bool): Match of creator plugin name is case sensitive.
            Set to `False` by default.

    Returns:
        Creator: Return first matching plugin or `None`.
    """

    # Lower input creator name if is not case sensitive
    if not case_sensitive:
        creator_name = creator_name.lower()

    for creator_plugin in discover_legacy_creator_plugins():
        _creator_name = creator_plugin.__name__

        # Lower creator plugin name if is not case sensitive
        if not case_sensitive:
            _creator_name = _creator_name.lower()

        if _creator_name == creator_name:
            return creator_plugin
    return None

get_next_versions_for_instances(project_name, instances)

Get next versions for instances by their folder path and product name.

Parameters:

Name Type Description Default
project_name str

Project name.

required
instances list[CreatedInstance]

Instances to get next versions for.

required

Returns:

Type Description

dict[str, Union[int, None]]: Next versions by instance id. Version is 'None' if instance has no folder path or product name.

Source code in client/ayon_core/pipeline/create/utils.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def get_next_versions_for_instances(project_name, instances):
    """Get next versions for instances by their folder path and product name.

    Args:
        project_name (str): Project name.
        instances (list[CreatedInstance]): Instances to get next versions for.

    Returns:
        dict[str, Union[int, None]]: Next versions by instance id. Version is
            'None' if instance has no folder path or product name.
    """

    last_versions = get_last_versions_for_instances(
        project_name, instances, True)

    output = {}
    for instance_id, version in last_versions.items():
        if version == -2:
            output[instance_id] = None
        elif version == -1:
            output[instance_id] = 1
        else:
            output[instance_id] = version + 1
    return output

get_product_name(project_name, task_name, task_type, host_name, product_type, variant, default_template=None, dynamic_data=None, project_settings=None, product_type_filter=None, project_entity=None)

Calculate product name based on passed context and AYON settings.

Subst name templates are defined in project_settings/global/tools/creator /product_name_profiles where are profiles with host name, product type, task name and task type filters. If context does not match any profile then DEFAULT_PRODUCT_TEMPLATE is used as default template.

That's main reason why so many arguments are required to calculate product name.

Todos

Find better filtering options to avoid requirement of argument 'family_filter'.

Parameters:

Name Type Description Default
project_name str

Project name.

required
task_name Union[str, None]

Task name.

required
task_type Union[str, None]

Task type.

required
host_name str

Host name.

required
product_type str

Product type.

required
variant str

In most of the cases it is user input during creation.

required
default_template Optional[str]

Default template if any profile does not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if is not passed.

None
dynamic_data Optional[Dict[str, Any]]

Dynamic data specific for a creator which creates instance.

None
project_settings Optional[Union[Dict[str, Any]]]

Prepared settings for project. Settings are queried if not passed.

None
product_type_filter Optional[str]

Use different product type for product template filtering. Value of product_type is used when not passed.

None
project_entity Optional[Dict[str, Any]]

Project entity used when task short name is required by template.

None

Returns:

Name Type Description
str

Product name.

Raises:

Type Description
TaskNotSetError

If template requires task which is not provided.

TemplateFillError

If filled template contains placeholder key which is not collected.

Source code in client/ayon_core/pipeline/create/product_name.py
 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
def get_product_name(
    project_name,
    task_name,
    task_type,
    host_name,
    product_type,
    variant,
    default_template=None,
    dynamic_data=None,
    project_settings=None,
    product_type_filter=None,
    project_entity=None,
):
    """Calculate product name based on passed context and AYON settings.

    Subst name templates are defined in `project_settings/global/tools/creator
    /product_name_profiles` where are profiles with host name, product type,
    task name and task type filters. If context does not match any profile
    then `DEFAULT_PRODUCT_TEMPLATE` is used as default template.

    That's main reason why so many arguments are required to calculate product
    name.

    Todos:
        Find better filtering options to avoid requirement of
            argument 'family_filter'.

    Args:
        project_name (str): Project name.
        task_name (Union[str, None]): Task name.
        task_type (Union[str, None]): Task type.
        host_name (str): Host name.
        product_type (str): Product type.
        variant (str): In most of the cases it is user input during creation.
        default_template (Optional[str]): Default template if any profile does
            not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
            is used if is not passed.
        dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for
            a creator which creates instance.
        project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings
            for project. Settings are queried if not passed.
        product_type_filter (Optional[str]): Use different product type for
            product template filtering. Value of `product_type` is used when
            not passed.
        project_entity (Optional[Dict[str, Any]]): Project entity used when
            task short name is required by template.

    Returns:
        str: Product name.

    Raises:
        TaskNotSetError: If template requires task which is not provided.
        TemplateFillError: If filled template contains placeholder key which
            is not collected.

    """
    if not product_type:
        return ""

    template = get_product_name_template(
        project_name,
        product_type_filter or product_type,
        task_name,
        task_type,
        host_name,
        default_template=default_template,
        project_settings=project_settings
    )
    # Simple check of task name existence for template with {task} in
    #   - missing task should be possible only in Standalone publisher
    if not task_name and "{task" in template.lower():
        raise TaskNotSetError()

    task_value = {
        "name": task_name,
        "type": task_type,
    }
    if "{task}" in template.lower():
        task_value = task_name

    elif "{task[short]}" in template.lower():
        if project_entity is None:
            project_entity = ayon_api.get_project(project_name)
        task_types_by_name = {
            task["name"]: task for task in
            project_entity["taskTypes"]
        }
        task_short = task_types_by_name.get(task_type, {}).get("shortName")
        task_value["short"] = task_short

    fill_pairs = {
        "variant": variant,
        "family": product_type,
        "task": task_value,
        "product": {
            "type": product_type
        }
    }
    if dynamic_data:
        # Dynamic data may override default values
        for key, value in dynamic_data.items():
            fill_pairs[key] = value

    try:
        return StringTemplate.format_strict_template(
            template=template,
            data=prepare_template_data(fill_pairs)
        )
    except KeyError as exp:
        raise TemplateFillError(
            "Value for {} key is missing in template '{}'."
            " Available values are {}".format(str(exp), template, fill_pairs)
        )

get_product_name_template(project_name, product_type, task_name, task_type, host_name, default_template=None, project_settings=None)

Get product name template based on passed context.

Parameters:

Name Type Description Default
project_name str

Project on which the context lives.

required
product_type str

Product type for which the product name is calculated.

required
host_name str

Name of host in which the product name is calculated.

required
task_name str

Name of task in which context the product is created.

required
task_type str

Type of task in which context the product is created.

required
default_template Union[str, None]

Default template which is used if settings won't find any matching possibility. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if not defined.

None
project_settings Union[Dict[str, Any], None]

Prepared settings for project. Settings are queried if not passed.

None
Source code in client/ayon_core/pipeline/create/product_name.py
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
def get_product_name_template(
    project_name,
    product_type,
    task_name,
    task_type,
    host_name,
    default_template=None,
    project_settings=None
):
    """Get product name template based on passed context.

    Args:
        project_name (str): Project on which the context lives.
        product_type (str): Product type for which the product name is
            calculated.
        host_name (str): Name of host in which the product name is calculated.
        task_name (str): Name of task in which context the product is created.
        task_type (str): Type of task in which context the product is created.
        default_template (Union[str, None]): Default template which is used if
            settings won't find any matching possibility. Constant
            'DEFAULT_PRODUCT_TEMPLATE' is used if not defined.
        project_settings (Union[Dict[str, Any], None]): Prepared settings for
            project. Settings are queried if not passed.
    """

    if project_settings is None:
        project_settings = get_project_settings(project_name)
    tools_settings = project_settings["core"]["tools"]
    profiles = tools_settings["creator"]["product_name_profiles"]
    filtering_criteria = {
        "product_types": product_type,
        "hosts": host_name,
        "tasks": task_name,
        "task_types": task_type
    }

    matching_profile = filter_profiles(profiles, filtering_criteria)
    template = None
    if matching_profile:
        # TODO remove formatting keys replacement
        template = (
            matching_profile["template"]
            .replace("{task[name]}", "{task}")
            .replace("{Task[name]}", "{Task}")
            .replace("{TASK[NAME]}", "{TASK}")
            .replace("{product[type]}", "{family}")
            .replace("{Product[type]}", "{Family}")
            .replace("{PRODUCT[TYPE]}", "{FAMILY}")
            .replace("{folder[name]}", "{asset}")
            .replace("{Folder[name]}", "{Asset}")
            .replace("{FOLDER[NAME]}", "{ASSET}")
        )

    # Make sure template is set (matching may have empty string)
    if not template:
        template = default_template or DEFAULT_PRODUCT_TEMPLATE
    return template