Skip to content

push

ensure_task_status(project, task_status_name) async

TODO: kitsu listener for new task statuses would be preferable

Source code in server/kitsu/push.py
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
async def ensure_task_status(
    project: "ProjectEntity",
    task_status_name: str,
) -> bool:
    """#TODO: kitsu listener for new task statuses would be preferable"""

    if task_status_name not in [
        status["name"]
        for status in project.statuses
    ]:
        logging.info(
            f"Creating task status {task_status_name} for '{project.name}'"
        )
        project.statuses.append(
            {
                "name": task_status_name,
                "icon": "task_alt",
                "shortName": task_status_name[:4],
            }
        )
        await project.save()
        return True
    return False

ensure_task_type(project, task_type_name) async

TODO: kitsu listener for new task types would be preferable

Source code in server/kitsu/push.py
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
async def ensure_task_type(
    project: "ProjectEntity",
    task_type_name: str,
) -> bool:
    """#TODO: kitsu listener for new task types would be preferable"""
    if task_type_name not in [
        task_type["name"]
        for task_type in project.task_types
    ]:
        logging.info(
            f"Creating task type {task_type_name} for '{project.name}'"
        )
        project.task_types.append(
            {
                "name": task_type_name,
                "shortName": task_type_name[:4],
                "icon": "task_alt",
            }
        )
        await project.save()
        return True
    return False

get_root_folder_id(user, project_name, kitsu_type, kitsu_type_id, subfolder_id=None, subfolder_name=None) async

Get the root folder ID for a given Kitsu type and ID. If a folder/subfolder does not exist, it will be created.

Source code in server/kitsu/push.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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
async def get_root_folder_id(
    user: "UserEntity",
    project_name: str,
    kitsu_type: KitsuEntityType,
    kitsu_type_id: str,
    subfolder_id: str | None = None,
    subfolder_name: str | None = None,
) -> str:
    """
    Get the root folder ID for a given Kitsu type and ID.
    If a folder/subfolder does not exist, it will be created.
    """
    res = await Postgres.fetch(
        f"""
        SELECT id FROM project_{project_name}.folders
        WHERE data->>'kitsuId' = $1
        """,
        kitsu_type_id,
    )

    if res:
        id = res[0]["id"]
    else:
        folder = await create_folder(
            project_name=project_name,
            name=kitsu_type,
            data={"kitsuId": kitsu_type_id},
        )
        id = folder.id

    if not (subfolder_id or subfolder_name):
        return id

    res = await Postgres.fetch(
        f"""
        SELECT id FROM project_{project_name}.folders
        WHERE data->>'kitsuId' = $1
        """,
        subfolder_id,
    )

    if res:
        sub_id = res[0]["id"]
    else:
        sub_folder = await create_folder(
            project_name=project_name,
            name=subfolder_name,
            parent_id=id,
            data={"kitsuId": subfolder_id},
        )
        sub_id = sub_folder.id
    return sub_id

sync_casting(addon, user, project, entity_dict, settings) async

Sync casting links for a target (shot or asset) by reconciling existing links with desired state from Kitsu.

Perform full reconciliation of casting links: 1. Fetch existing AYON links for the target folder 2. Map existing links by asset Kitsu ID (from link data or folder lookup) 3. For each asset in Kitsu's desired state: - Delete excess links if more exist in AYON than in Kitsu - Create missing links if fewer exist in AYON than in Kitsu - Each link includes occurence number in its data 4. Delete links for assets removed from Kitsu casting

Handle multi-occurence casting by creating multiple links with the same input/output/type but different occurence numbers.

Parameters:

Name Type Description Default
addon KitsuAddon

KitsuAddon instance (unused but required by push_entities).

required
user UserEntity

User entity for authentication when creating/deleting links.

required
project ProjectEntity

AYON project entity.

required
entity_dict EntityDict

SyncCasting entity dictionary containing: - target_id: Kitsu ID of the shot or asset - target_type: "Shot" or "Asset" - asset_ids: Dictionary mapping asset Kitsu IDs to occurence counts - ayon_server_url: Base URL for AYON API calls

required
settings Any

Addon settings containing sync_casting configuration.

required

Returns:

Type Description
None

None. Warnings for missing targets/assets and API failures.

Source code in server/kitsu/push.py
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
async def sync_casting(
    addon: "KitsuAddon",
    user: "UserEntity",
    project: "ProjectEntity",
    entity_dict: "EntityDict",
    settings: Any,
) -> None:
    """Sync casting links for a target (shot or asset) by reconciling
    existing links with desired state from Kitsu.

    Perform full reconciliation of casting links:
    1. Fetch existing AYON links for the target folder
    2. Map existing links by asset Kitsu ID (from link data or folder lookup)
    3. For each asset in Kitsu's desired state:
       - Delete excess links if more exist in AYON than in Kitsu
       - Create missing links if fewer exist in AYON than in Kitsu
       - Each link includes occurence number in its data
    4. Delete links for assets removed from Kitsu casting

    Handle multi-occurence casting by creating multiple links
    with the same input/output/type but different occurence numbers.

    Args:
        addon: KitsuAddon instance (unused but required by push_entities).
        user: User entity for authentication when creating/deleting links.
        project: AYON project entity.
        entity_dict: SyncCasting entity dictionary containing:
            - target_id: Kitsu ID of the shot or asset
            - target_type: "Shot" or "Asset"
            - asset_ids: Dictionary mapping asset Kitsu IDs to occurence
              counts
            - ayon_server_url: Base URL for AYON API calls
        settings: Addon settings containing sync_casting configuration.

    Returns:
        None. Warnings for missing targets/assets and API failures.
    """
    if not settings.sync_settings.sync_casting.enabled:
        return

    target_kitsu_id = entity_dict.get("target_id")
    asset_ids = entity_dict.get("asset_ids", {})

    if not target_kitsu_id:
        logging.warning("SyncCasting missing target_id")
        return

    # Get target folder (shot or asset)
    target_folder = await get_folder_by_kitsu_id(
        project.name,
        target_kitsu_id,
    )
    if not target_folder:
        logging.warning(
            f"SyncCasting target not found for kitsuId {target_kitsu_id}"
        )
        return

    # Get link type from settings
    link_type = (
        settings.sync_settings.sync_casting.casting_link_type or "breakdown"
    )
    # Ensure link_type has proper format: name|input_type|output_type
    if "|" not in link_type:
        link_type = f"{link_type}|folder|folder"

    # Get existing AYON links for this target
    existing_links = await get_links_for_output(
        project.name,
        target_folder.id,
        link_type,
    )

    # Build mapping of asset_kitsu_id -> list of existing links
    existing_links_by_asset: dict[str, list[dict]] = {}

    for link in existing_links:
        # Try to get asset_kitsu_id from link data
        link_data = link.get("data")
        if link_data and isinstance(link_data, dict):
            asset_kitsu_id = link_data.get("kitsuAssetId")
        else:
            asset_kitsu_id = None

        if not asset_kitsu_id:
            # Fallback: try to find asset by input_id by querying the folder
            input_id = link.get("input_id")
            if input_id:
                try:
                    folder = await FolderEntity.load(project.name, input_id)
                    asset_kitsu_id = folder.data.get("kitsuId")
                except Exception:
                    # Folder not found or no kitsuId, skip this link
                    continue

        if asset_kitsu_id:
            if asset_kitsu_id not in existing_links_by_asset:
                existing_links_by_asset[asset_kitsu_id] = []
            existing_links_by_asset[asset_kitsu_id].append(link)

    # Process each asset with its desired count
    for asset_kitsu_id, desired_count in asset_ids.items():
        asset_folder = await get_folder_by_kitsu_id(
            project.name,
            asset_kitsu_id,
        )
        if not asset_folder:
            logging.debug(
                f"SyncCasting asset not found for kitsuId "
                f"{asset_kitsu_id}, skipping"
            )
            continue

        existing_count = len(existing_links_by_asset.get(asset_kitsu_id, []))

        # Delete excess links (more in AYON than in Kitsu)
        if existing_count > desired_count:
            links_to_delete = existing_links_by_asset[asset_kitsu_id][
                : existing_count - desired_count
            ]
            for link in links_to_delete:
                await delete_entity_link(
                    project_name=project.name,
                    user=user,
                    ayon_server_url=entity_dict["ayon_server_url"],
                    link_id=link["id"],
                )
                logging.debug(
                    f"Deleted excess casting link "
                    f"{link.get('input_id')}->{target_folder.id} "
                    f"(asset {asset_kitsu_id}, had {existing_count}, "
                    f"need {desired_count})"
                )

        # Create missing links (more in Kitsu than in AYON)
        elif existing_count < desired_count:
            for occurence_num in range(existing_count + 1, desired_count + 1):
                await create_entity_link(
                    project_name=project.name,
                    user=user,
                    ayon_server_url=entity_dict["ayon_server_url"],
                    input_id=asset_folder.id,
                    output_id=target_folder.id,
                    link_type=link_type,
                    data={
                        "kitsuAssetId": asset_kitsu_id,
                        "kitsuTargetId": target_kitsu_id,
                        "occurence": occurence_num,
                    },
                )
                logging.debug(
                    f"Created casting link "
                    f"{asset_folder.name}->{target_folder.name} "
                    f"(asset {asset_kitsu_id}, occurence "
                    f"{occurence_num}/{desired_count})"
                )

    # Handle assets that were removed from Kitsu
    # (exist in AYON but not in count dict)
    # Find links for assets not in the count dict
    for link in existing_links:
        # Try to get asset_kitsu_id from link data
        link_data = link.get("data")
        if link_data and isinstance(link_data, dict):
            asset_kitsu_id = link_data.get("kitsuAssetId")
        else:
            asset_kitsu_id = None

        if not asset_kitsu_id:
            # Fallback: try to find asset by input_id
            input_id = link.get("input_id")
            if input_id:
                try:
                    folder = await FolderEntity.load(project.name, input_id)
                    asset_kitsu_id = folder.data.get("kitsuId")
                except Exception:
                    # Folder not found, skip
                    continue

        # If this asset is not in the asset_ids dict, it was removed from Kitsu
        if asset_kitsu_id and asset_kitsu_id not in asset_ids:
            await delete_entity_link(
                project_name=project.name,
                user=user,
                ayon_server_url=entity_dict["ayon_server_url"],
                link_id=link["id"],
            )
            logging.debug(
                f"Deleted stale casting link "
                f"{link.get('input_id')}->{target_folder.id} "
                f"(asset {asset_kitsu_id} removed from Kitsu)"
            )