Skip to content

templates

AnatomyStringTemplate

Bases: StringTemplate

String template which has access to anatomy.

Parameters:

Name Type Description Default
anatomy_templates AnatomyTemplates

Anatomy templates object.

required
template str

Template string.

required
Source code in client/ayon_core/pipeline/anatomy/templates.py
 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
class AnatomyStringTemplate(StringTemplate):
    """String template which has access to anatomy.

    Args:
        anatomy_templates (AnatomyTemplates): Anatomy templates object.
        template (str): Template string.
    """

    def __init__(self, anatomy_templates, template):
        self.anatomy_templates = anatomy_templates
        super(AnatomyStringTemplate, self).__init__(template)

    def format(self, data):
        """Format template and add 'root' key to data if not available.

        Args:
            data (dict[str, Any]): Formatting data for template.

        Returns:
            AnatomyTemplateResult: Formatting result.
        """

        anatomy_templates = self.anatomy_templates
        if not data.get("root"):
            data = copy.deepcopy(data)
            data["root"] = anatomy_templates.anatomy.roots
        result = StringTemplate.format(self, data)
        rootless_path = anatomy_templates.get_rootless_path_from_result(
            result
        )
        return AnatomyTemplateResult(result, rootless_path)

format(data)

Format template and add 'root' key to data if not available.

Parameters:

Name Type Description Default
data dict[str, Any]

Formatting data for template.

required

Returns:

Name Type Description
AnatomyTemplateResult

Formatting result.

Source code in client/ayon_core/pipeline/anatomy/templates.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def format(self, data):
    """Format template and add 'root' key to data if not available.

    Args:
        data (dict[str, Any]): Formatting data for template.

    Returns:
        AnatomyTemplateResult: Formatting result.
    """

    anatomy_templates = self.anatomy_templates
    if not data.get("root"):
        data = copy.deepcopy(data)
        data["root"] = anatomy_templates.anatomy.roots
    result = StringTemplate.format(self, data)
    rootless_path = anatomy_templates.get_rootless_path_from_result(
        result
    )
    return AnatomyTemplateResult(result, rootless_path)

AnatomyTemplateResult

Bases: TemplateResult

Source code in client/ayon_core/pipeline/anatomy/templates.py
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
class AnatomyTemplateResult(TemplateResult):
    rootless = None

    def __new__(cls, result, rootless_path):
        new_obj = super(AnatomyTemplateResult, cls).__new__(
            cls,
            str(result),
            result.template,
            result.solved,
            result.used_values,
            result.missing_keys,
            result.invalid_types
        )
        new_obj.rootless = rootless_path
        return new_obj

    def validate(self):
        if not self.solved:
            raise AnatomyTemplateUnsolved(
                self.template,
                self.missing_keys,
                self.invalid_types
            )

    def copy(self):
        tmp = TemplateResult(
            str(self),
            self.template,
            self.solved,
            self.used_values,
            self.missing_keys,
            self.invalid_types
        )
        return self.__class__(tmp, self.rootless)

    def normalized(self):
        """Convert to normalized path."""

        tmp = TemplateResult(
            os.path.normpath(self),
            self.template,
            self.solved,
            self.used_values,
            self.missing_keys,
            self.invalid_types
        )
        return self.__class__(tmp, self.rootless)

normalized()

Convert to normalized path.

Source code in client/ayon_core/pipeline/anatomy/templates.py
56
57
58
59
60
61
62
63
64
65
66
67
def normalized(self):
    """Convert to normalized path."""

    tmp = TemplateResult(
        os.path.normpath(self),
        self.template,
        self.solved,
        self.used_values,
        self.missing_keys,
        self.invalid_types
    )
    return self.__class__(tmp, self.rootless)

AnatomyTemplates

Source code in client/ayon_core/pipeline/anatomy/templates.py
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
class AnatomyTemplates:
    inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})")
    inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}")

    def __init__(self, anatomy):
        self._anatomy = anatomy

        self._loaded_project = None
        self._raw_templates = None
        self._templates = None
        self._objected_templates = None

    def __getitem__(self, key):
        self._validate_discovery()
        return self._objected_templates[key]

    def get(self, key, default=None):
        self._validate_discovery()
        return self._objected_templates.get(key, default)

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

    def reset(self):
        self._raw_templates = None
        self._templates = None
        self._objected_templates = None

    @property
    def anatomy(self):
        """Anatomy instance.

        Returns:
            Anatomy: Anatomy instance.

        """
        return self._anatomy

    @property
    def project_name(self):
        """Project name.

        Returns:
            Union[str, None]: Project name if set, otherwise None.

        """
        return self._anatomy.project_name

    @property
    def roots(self):
        """Anatomy roots object.

        Returns:
            RootItem: Anatomy roots data.

        """
        return self._anatomy.roots

    @property
    def templates(self):
        """Templates data.

        Templates data with replaced common data.

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

        """
        self._validate_discovery()
        return self._templates

    @property
    def frame_padding(self):
        """Default frame padding.

        Returns:
            int: Frame padding used by default in templates.

        """
        self._validate_discovery()
        return self["frame_padding"]

    @property
    def version_padding(self):
        """Default version padding.

        Returns:
            int: Version padding used by default in templates.

        """
        self._validate_discovery()
        return self["version_padding"]

    @classmethod
    def get_rootless_path_from_result(cls, result):
        """Calculate rootless path from formatting result.

        Args:
            result (TemplateResult): Result of StringTemplate formatting.

        Returns:
            str: Rootless path if result contains one of anatomy roots.
        """

        used_values = result.used_values
        missing_keys = result.missing_keys
        template = result.template
        invalid_types = result.invalid_types
        if (
            "root" not in used_values
            or "root" in missing_keys
            or "{root" not in template
        ):
            return

        for invalid_type in invalid_types:
            if "root" in invalid_type:
                return

        root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]})
        if not root_keys:
            return

        output = str(result)
        for used_root_keys in root_keys:
            if not used_root_keys:
                continue

            used_value = used_values
            root_key = None
            for key in used_root_keys:
                used_value = used_value[key]
                if root_key is None:
                    root_key = key
                else:
                    root_key += "[{}]".format(key)

            root_key = "{" + root_key + "}"
            output = output.replace(str(used_value), root_key)

        return output

    def format(self, data, strict=True):
        """Fill all templates based on entered data.

        Args:
            data (dict[str, Any]): Fill data used for template formatting.
            strict (Optional[bool]): Raise exception is accessed value is
                not fully filled.

        Returns:
            TemplatesResultDict: Output `TemplateResult` have `strict`
                attribute set to False so accessing unfilled keys in templates
                won't raise any exceptions.

        """
        self._validate_discovery()
        copy_data = copy.deepcopy(data)
        roots = self._anatomy.roots
        if roots:
            copy_data["root"] = roots

        return self._solve_dict(copy_data, strict)

    def format_all(self, in_data):
        """Fill all templates based on entered data.

        Deprecated:
            Use `format` method with `strict=False` instead.

        Args:
            in_data (dict): Containing keys to be filled into template.

        Returns:
            TemplatesResultDict: Output `TemplateResult` have `strict`
                attribute set to False so accessing unfilled keys in templates
                won't raise any exceptions.

        """
        return self.format(in_data, strict=False)

    def get_template_item(
        self, category_name, template_name, subkey=None, default=_PLACEHOLDER
    ):
        """Get template item from category.

        Args:
            category_name (str): Category name.
            template_name (str): Template name.
            subkey (Optional[str]): Subkey name.
            default (Any): Default value if template is not found.

        Returns:
            Any: Template item or subkey value.

        Raises:
            KeyError: When any passed key is not available. Raise of error
                does not happen if 'default' is filled.

        """
        self._validate_discovery()
        category = self.get(category_name)
        if category is None:
            if default is not _PLACEHOLDER:
                return default
            raise KeyError("Category '{}' not found.".format(category_name))

        template_item = category.get(template_name)
        if template_item is None:
            if default is not _PLACEHOLDER:
                return default
            raise KeyError(
                "Template '{}' not found in category '{}'.".format(
                    template_name, category_name
                )
            )

        if subkey is None:
            return template_item

        item = template_item.get(subkey)
        if item is not None:
            return item

        if default is not _PLACEHOLDER:
            return default
        raise KeyError(
            "Subkey '{}' not found in '{}/{}'.".format(
                subkey, category_name, template_name
            )
        )

    def _solve_dict(self, data, strict):
        """ Solves templates with entered data.

        Args:
            data (dict): Containing keys to be filled into template.

        Returns:
            dict: With `TemplateResult` in values containing filled or
                partially filled templates.

        """
        output = {}
        for key, value in self._objected_templates.items():
            if isinstance(value, TemplateCategory):
                value = value.format(data, strict)
            elif isinstance(value, AnatomyStringTemplate):
                value = value.format(data)
            output[key] = value
        return TemplatesResultDict(output, strict=strict)

    def _validate_discovery(self):
        """Validate if templates are discovered and loaded for anatomy project.

        When project changes the cached data are reset and discovered again.
        """
        if self.project_name != self._loaded_project:
            self.reset()

        if self._templates is None:
            self._discover()
            self._loaded_project = self.project_name

    def _create_objected_templates(self, templates):
        """Create objected templates from templates data.

        Args:
            templates (dict[str, Any]): Templates data from project entity.

        Returns:
            dict[str, Any]: Values are cnmverted to template objects.

        """
        objected_templates = {}
        for category_name, category_value in copy.deepcopy(templates).items():
            if isinstance(category_value, dict):
                category_value = TemplateCategory(
                    self, category_name, category_value
                )
            elif isinstance(category_value, str):
                category_value = AnatomyStringTemplate(self, category_value)
            objected_templates[category_name] = category_value
        return objected_templates

    def _discover(self):
        """Load and cache templates from project entity."""
        if self.project_name is None:
            raise ProjectNotSet("Anatomy project is not set.")

        templates = self.anatomy["templates"]
        self._raw_templates = copy.deepcopy(templates)

        templates = copy.deepcopy(templates)
        # Make sure all the keys are available
        for key in (
            "publish",
            "hero",
            "work",
            "delivery",
            "staging",
            "others",
        ):
            templates.setdefault(key, {})

        solved_templates = self._solve_template_inner_links(templates)
        self._templates = solved_templates
        self._objected_templates = self._create_objected_templates(
            solved_templates
        )

    @classmethod
    def _replace_inner_keys(cls, matches, value, key_values, key):
        """Replacement of inner keys in template values."""
        for match in matches:
            anatomy_sub_keys = (
                cls.inner_key_name_pattern.findall(match)
            )
            if key in anatomy_sub_keys:
                raise ValueError((
                    "Unsolvable recursion in inner keys, "
                    "key: \"{}\" is in his own value."
                    " Can't determine source, please check Anatomy templates."
                ).format(key))

            for anatomy_sub_key in anatomy_sub_keys:
                replace_value = key_values.get(anatomy_sub_key)
                if replace_value is None:
                    raise KeyError((
                        "Anatomy templates can't be filled."
                        " Anatomy key `{0}` has"
                        " invalid inner key `{1}`."
                    ).format(key, anatomy_sub_key))

                if not (
                    isinstance(replace_value, numbers.Number)
                    or isinstance(replace_value, str)
                ):
                    raise ValueError((
                        "Anatomy templates can't be filled."
                        " Anatomy key `{0}` has"
                        " invalid inner key `{1}`"
                        " with value `{2}`."
                    ).format(key, anatomy_sub_key, str(replace_value)))

                value = value.replace(match, str(replace_value))

        return value

    @classmethod
    def _prepare_inner_keys(cls, key_values):
        """Check values of inner keys.

        Check if inner key exist in template group and has valid value.
        It is also required to avoid infinite loop with unsolvable recursion
        when first inner key's value refers to second inner key's value where
        first is used.
        """
        keys_to_solve = set(key_values.keys())
        while True:
            found = False
            for key in tuple(keys_to_solve):
                value = key_values[key]

                if isinstance(value, str):
                    matches = cls.inner_key_pattern.findall(value)
                    if not matches:
                        keys_to_solve.remove(key)
                        continue

                    found = True
                    key_values[key] = cls._replace_inner_keys(
                        matches, value, key_values, key
                    )
                    continue

                elif not isinstance(value, dict):
                    keys_to_solve.remove(key)
                    continue

                subdict_found = False
                for _key, _value in tuple(value.items()):
                    matches = cls.inner_key_pattern.findall(_value)
                    if not matches:
                        continue

                    subdict_found = True
                    found = True
                    key_values[key][_key] = cls._replace_inner_keys(
                        matches, _value, key_values,
                        "{}.{}".format(key, _key)
                    )

                if not subdict_found:
                    keys_to_solve.remove(key)

            if not found:
                break

        return key_values

    @classmethod
    def _solve_template_inner_links(cls, templates):
        """Solve templates inner keys identified by "{@*}".

        Process is split into 2 parts.
        First is collecting all global keys (keys in top hierarchy where value
        is not dictionary). All global keys are set for all group keys (keys
        in top hierarchy where value is dictionary). Value of a key is not
        overridden in group if already contain value for the key.

        In second part all keys with "at" symbol in value are replaced with
        value of the key afterward "at" symbol from the group.

        Args:
            templates (dict): Raw templates data.

        Example:
            templates::
                key_1: "value_1",
                key_2: "{@key_1}/{filling_key}"

                group_1:
                    key_3: "value_3/{@key_2}"

                group_2:
                    key_2": "value_2"
                    key_4": "value_4/{@key_2}"

            output::
                key_1: "value_1"
                key_2: "value_1/{filling_key}"

                group_1: {
                    key_1: "value_1"
                    key_2: "value_1/{filling_key}"
                    key_3: "value_3/value_1/{filling_key}"

                group_2: {
                    key_1: "value_1"
                    key_2: "value_2"
                    key_4: "value_3/value_2"

        Returns:
            dict[str, Any]: Solved templates data.

        """
        default_key_values = templates.pop("common", {})
        output = {}
        for category_name, category_value in templates.items():
            new_category_value = {}
            for key, value in category_value.items():
                key_values = copy.deepcopy(default_key_values)
                key_values.update(value)
                new_category_value[key] = cls._prepare_inner_keys(key_values)
            output[category_name] = new_category_value

        default_keys_by_subkeys = cls._prepare_inner_keys(default_key_values)
        for key, value in default_keys_by_subkeys.items():
            output[key] = value

        return output

    @classmethod
    def _dict_to_subkeys_list(cls, subdict):
        """Convert dictionary to list of subkeys.

        Example::

            _dict_to_subkeys_list({
                "root": {
                    "work": "path/to/work",
                    "publish": "path/to/publish"
                }
            })
            [
                ["root", "work"],
                ["root", "publish"]
            ]


        Args:
            dict[str, Any]: Dictionary to be converted.

        Returns:
            list[list[str]]: List of subkeys.

        """
        output = []
        subkey_queue = collections.deque()
        subkey_queue.append((subdict, []))
        while subkey_queue:
            queue_item = subkey_queue.popleft()
            data, pre_keys = queue_item
            for key, value in data.items():
                result = list(pre_keys)
                result.append(key)
                if isinstance(value, dict):
                    subkey_queue.append((value, result))
                else:
                    output.append(result)
        return output

anatomy property

Anatomy instance.

Returns:

Name Type Description
Anatomy

Anatomy instance.

frame_padding property

Default frame padding.

Returns:

Name Type Description
int

Frame padding used by default in templates.

project_name property

Project name.

Returns:

Type Description

Union[str, None]: Project name if set, otherwise None.

roots property

Anatomy roots object.

Returns:

Name Type Description
RootItem

Anatomy roots data.

templates property

Templates data.

Templates data with replaced common data.

Returns:

Type Description

dict[str, Any]: Templates data.

version_padding property

Default version padding.

Returns:

Name Type Description
int

Version padding used by default in templates.

format(data, strict=True)

Fill all templates based on entered data.

Parameters:

Name Type Description Default
data dict[str, Any]

Fill data used for template formatting.

required
strict Optional[bool]

Raise exception is accessed value is not fully filled.

True

Returns:

Name Type Description
TemplatesResultDict

Output TemplateResult have strict attribute set to False so accessing unfilled keys in templates won't raise any exceptions.

Source code in client/ayon_core/pipeline/anatomy/templates.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
def format(self, data, strict=True):
    """Fill all templates based on entered data.

    Args:
        data (dict[str, Any]): Fill data used for template formatting.
        strict (Optional[bool]): Raise exception is accessed value is
            not fully filled.

    Returns:
        TemplatesResultDict: Output `TemplateResult` have `strict`
            attribute set to False so accessing unfilled keys in templates
            won't raise any exceptions.

    """
    self._validate_discovery()
    copy_data = copy.deepcopy(data)
    roots = self._anatomy.roots
    if roots:
        copy_data["root"] = roots

    return self._solve_dict(copy_data, strict)

format_all(in_data)

Fill all templates based on entered data.

Deprecated

Use format method with strict=False instead.

Parameters:

Name Type Description Default
in_data dict

Containing keys to be filled into template.

required

Returns:

Name Type Description
TemplatesResultDict

Output TemplateResult have strict attribute set to False so accessing unfilled keys in templates won't raise any exceptions.

Source code in client/ayon_core/pipeline/anatomy/templates.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def format_all(self, in_data):
    """Fill all templates based on entered data.

    Deprecated:
        Use `format` method with `strict=False` instead.

    Args:
        in_data (dict): Containing keys to be filled into template.

    Returns:
        TemplatesResultDict: Output `TemplateResult` have `strict`
            attribute set to False so accessing unfilled keys in templates
            won't raise any exceptions.

    """
    return self.format(in_data, strict=False)

get_rootless_path_from_result(result) classmethod

Calculate rootless path from formatting result.

Parameters:

Name Type Description Default
result TemplateResult

Result of StringTemplate formatting.

required

Returns:

Name Type Description
str

Rootless path if result contains one of anatomy roots.

Source code in client/ayon_core/pipeline/anatomy/templates.py
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
@classmethod
def get_rootless_path_from_result(cls, result):
    """Calculate rootless path from formatting result.

    Args:
        result (TemplateResult): Result of StringTemplate formatting.

    Returns:
        str: Rootless path if result contains one of anatomy roots.
    """

    used_values = result.used_values
    missing_keys = result.missing_keys
    template = result.template
    invalid_types = result.invalid_types
    if (
        "root" not in used_values
        or "root" in missing_keys
        or "{root" not in template
    ):
        return

    for invalid_type in invalid_types:
        if "root" in invalid_type:
            return

    root_keys = cls._dict_to_subkeys_list({"root": used_values["root"]})
    if not root_keys:
        return

    output = str(result)
    for used_root_keys in root_keys:
        if not used_root_keys:
            continue

        used_value = used_values
        root_key = None
        for key in used_root_keys:
            used_value = used_value[key]
            if root_key is None:
                root_key = key
            else:
                root_key += "[{}]".format(key)

        root_key = "{" + root_key + "}"
        output = output.replace(str(used_value), root_key)

    return output

get_template_item(category_name, template_name, subkey=None, default=_PLACEHOLDER)

Get template item from category.

Parameters:

Name Type Description Default
category_name str

Category name.

required
template_name str

Template name.

required
subkey Optional[str]

Subkey name.

None
default Any

Default value if template is not found.

_PLACEHOLDER

Returns:

Name Type Description
Any

Template item or subkey value.

Raises:

Type Description
KeyError

When any passed key is not available. Raise of error does not happen if 'default' is filled.

Source code in client/ayon_core/pipeline/anatomy/templates.py
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
def get_template_item(
    self, category_name, template_name, subkey=None, default=_PLACEHOLDER
):
    """Get template item from category.

    Args:
        category_name (str): Category name.
        template_name (str): Template name.
        subkey (Optional[str]): Subkey name.
        default (Any): Default value if template is not found.

    Returns:
        Any: Template item or subkey value.

    Raises:
        KeyError: When any passed key is not available. Raise of error
            does not happen if 'default' is filled.

    """
    self._validate_discovery()
    category = self.get(category_name)
    if category is None:
        if default is not _PLACEHOLDER:
            return default
        raise KeyError("Category '{}' not found.".format(category_name))

    template_item = category.get(template_name)
    if template_item is None:
        if default is not _PLACEHOLDER:
            return default
        raise KeyError(
            "Template '{}' not found in category '{}'.".format(
                template_name, category_name
            )
        )

    if subkey is None:
        return template_item

    item = template_item.get(subkey)
    if item is not None:
        return item

    if default is not _PLACEHOLDER:
        return default
    raise KeyError(
        "Subkey '{}' not found in '{}/{}'.".format(
            subkey, category_name, template_name
        )
    )

TemplateCategory

Template category.

Template category groups template items for specific usage. Categories available at the moment are 'work', 'publish', 'hero', 'delivery', 'staging' and 'others'.

Parameters:

Name Type Description Default
anatomy_templates AnatomyTemplates

Anatomy templates object.

required
category_name str

Category name.

required
category_data dict[str, Any]

Category data.

required
Source code in client/ayon_core/pipeline/anatomy/templates.py
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
class TemplateCategory:
    """Template category.

    Template category groups template items for specific usage. Categories
        available at the moment are 'work', 'publish', 'hero', 'delivery',
        'staging' and 'others'.

    Args:
        anatomy_templates (AnatomyTemplates): Anatomy templates object.
        category_name (str): Category name.
        category_data (dict[str, Any]): Category data.

    """
    def __init__(self, anatomy_templates, category_name, category_data):
        for key, value in category_data.items():
            if isinstance(value, dict):
                value = TemplateItem(anatomy_templates, value)
            elif isinstance(value, str):
                value = AnatomyStringTemplate(anatomy_templates, value)
            category_data[key] = value
        self._name = category_name
        self._name_prefix = "{}_".format(category_name)
        self._category_data = category_data

    def __getitem__(self, key):
        new_key = self._convert_getter_key(key)
        return self._category_data[new_key]

    def get(self, key, default=None):
        new_key = self._convert_getter_key(key)
        return self._category_data.get(new_key, default)

    @property
    def name(self):
        """Category name.

        Returns:
            str: Category name.

        """
        return self._name

    def format(self, data, strict=True):
        output = {}
        for key, value in self._category_data.items():
            if isinstance(value, TemplateItem):
                value = value.format(data, strict)
            elif isinstance(value, AnatomyStringTemplate):
                value = value.format(data)

            if isinstance(value, TemplatesResultDict):
                value.key = key
            output[key] = value
        return TemplatesResultDict(output, key=self.name, strict=strict)

    def _convert_getter_key(self, key):
        """Convert key for backwards compatibility.

        OpenPype compatible settings did contain template keys prefixed by
        category name e.g. 'publish_render' which should be just 'render'.

        This method keeps the backwards compatibility but only if the key
        starts with the category name prefix and the key is available in
        roots.

        Args:
            key (str): Key to be converted.

        Returns:
            str: Converted string.

        """
        if key in self._category_data:
            return key

        # Use default when the key is the category name
        if key == self._name:
            return "default"

        # Remove prefix if is key prefixed
        if key.startswith(self._name_prefix):
            new_key = key[len(self._name_prefix):]
            if new_key in self._category_data:
                return new_key
        return key

name property

Category name.

Returns:

Name Type Description
str

Category name.

TemplateItem

Template item under template category.

This item data usually contains 'file' and 'directory' by anatomy definition, enhanced by common data ('frame_padding', 'version_padding'). It adds 'path' key which is combination of 'file' and 'directory' values.

Parameters:

Name Type Description Default
anatomy_templates AnatomyTemplates

Anatomy templates object.

required
template_data dict[str, Any]

Templates data.

required
Source code in client/ayon_core/pipeline/anatomy/templates.py
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
class TemplateItem:
    """Template item under template category.

    This item data usually contains 'file' and 'directory' by anatomy
        definition, enhanced by common data ('frame_padding',
        'version_padding'). It adds 'path' key which is combination of
        'file' and 'directory' values.

    Args:
        anatomy_templates (AnatomyTemplates): Anatomy templates object.
        template_data (dict[str, Any]): Templates data.

    """
    def __init__(self, anatomy_templates, template_data):
        template_data = copy.deepcopy(template_data)

        # Backwards compatibility for 'folder'
        # TODO remove when deprecation not needed anymore
        if (
            "folder" not in template_data
            and "directory" in template_data
        ):
            template_data["folder"] = template_data["directory"]

        # Add 'path' key
        if (
            "path" not in template_data
            and "file" in template_data
            and "directory" in template_data
        ):
            template_data["path"] = "/".join(
                (template_data["directory"], template_data["file"])
            )

        for key, value in template_data.items():
            if isinstance(value, str):
                value = AnatomyStringTemplate(anatomy_templates, value)
            template_data[key] = value

        self._template_data = template_data
        self._anatomy_templates = anatomy_templates

    def __getitem__(self, key):
        return self._template_data[key]

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

    def format(self, data, strict=True):
        output = {}
        for key, value in self._template_data.items():
            if isinstance(value, AnatomyStringTemplate):
                value = value.format(data)
            output[key] = value
        return TemplatesResultDict(output, strict=strict)

TemplatesResultDict

Bases: dict

Holds and wrap 'AnatomyTemplateResult' for easy bug report.

Dictionary like object which holds 'AnatomyTemplateResult' in the same data structure as base dictionary of anatomy templates. It can raise

Source code in client/ayon_core/pipeline/anatomy/templates.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
class TemplatesResultDict(dict):
    """Holds and wrap 'AnatomyTemplateResult' for easy bug report.

    Dictionary like object which holds 'AnatomyTemplateResult' in the same
    data structure as base dictionary of anatomy templates. It can raise

    """

    def __init__(self, in_data, key=None, parent=None, strict=None):
        super(TemplatesResultDict, self).__init__()
        for _key, _value in in_data.items():
            if isinstance(_value, TemplatesResultDict):
                _value.parent = self
            elif isinstance(_value, dict):
                _value = self.__class__(_value, _key, self)
            self[_key] = _value

        if strict is None and parent is None:
            strict = True

        self.key = key
        self.parent = parent
        self._is_strict = strict

    def __getitem__(self, key):
        if key not in self.keys():
            hier = self.get_hierarchy()
            hier.append(key)
            raise TemplateMissingKey(hier)

        value = super(TemplatesResultDict, self).__getitem__(key)
        if isinstance(value, self.__class__):
            return value

        # Raise exception when expected solved templates and it is not.
        if self.is_strict and hasattr(value, "validate"):
            value.validate()
        return value

    def get_is_strict(self):
        return self._is_strict

    def set_is_strict(self, is_strict):
        if is_strict is None and self.parent is None:
            is_strict = True
        self._is_strict = is_strict
        for child in self.values():
            if isinstance(child, self.__class__):
                child.set_is_strict(is_strict)
            elif isinstance(child, AnatomyTemplateResult):
                child.strict = is_strict

    strict = property(get_is_strict, set_is_strict)
    is_strict = property(get_is_strict, set_is_strict)

    def get_hierarchy(self):
        """Return dictionary keys one by one to root parent."""
        if self.key is None:
            return []

        if self.parent is None:
            return [self.key]

        par_hier = list(self.parent.get_hierarchy())
        par_hier.append(self.key)
        return par_hier

    @property
    def missing_keys(self):
        """Return missing keys of all children templates."""
        missing_keys = set()
        for value in self.values():
            missing_keys |= value.missing_keys
        return missing_keys

    @property
    def invalid_types(self):
        """Return invalid types of all children templates."""
        invalid_types = {}
        for value in self.values():
            invalid_types = _merge_dict(invalid_types, value.invalid_types)
        return invalid_types

    @property
    def used_values(self):
        """Return used values for all children templates."""
        used_values = {}
        for value in self.values():
            used_values = _merge_dict(used_values, value.used_values)
        return used_values

    def get_solved(self):
        """Get only solved key from templates."""
        result = {}
        for key, value in self.items():
            if isinstance(value, self.__class__):
                value = value.get_solved()
                if not value:
                    continue
                result[key] = value

            elif (
                not hasattr(value, "solved") or
                value.solved
            ):
                result[key] = value
        return self.__class__(result, key=self.key, parent=self.parent)

invalid_types property

Return invalid types of all children templates.

missing_keys property

Return missing keys of all children templates.

used_values property

Return used values for all children templates.

get_hierarchy()

Return dictionary keys one by one to root parent.

Source code in client/ayon_core/pipeline/anatomy/templates.py
191
192
193
194
195
196
197
198
199
200
201
def get_hierarchy(self):
    """Return dictionary keys one by one to root parent."""
    if self.key is None:
        return []

    if self.parent is None:
        return [self.key]

    par_hier = list(self.parent.get_hierarchy())
    par_hier.append(self.key)
    return par_hier

get_solved()

Get only solved key from templates.

Source code in client/ayon_core/pipeline/anatomy/templates.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def get_solved(self):
    """Get only solved key from templates."""
    result = {}
    for key, value in self.items():
        if isinstance(value, self.__class__):
            value = value.get_solved()
            if not value:
                continue
            result[key] = value

        elif (
            not hasattr(value, "solved") or
            value.solved
        ):
            result[key] = value
    return self.__class__(result, key=self.key, parent=self.parent)