Skip to content

utils

check_sg_attribute_exists(sg_session, sg_entity_type, field_code, check_writable=False)

Validate whether given field code exists under that entity type

Source code in services/shotgrid_common/utils.py
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
def check_sg_attribute_exists(
    sg_session: shotgun_api3.Shotgun,
    sg_entity_type: str,
    field_code: str,
    check_writable: bool = False,
) -> bool:
    """Validate whether given field code exists under that entity type"""
    try:
        schema_field = sg_session.schema_field_read(
            sg_entity_type,
            field_name=field_code
        )
        # If we are checking whether the attribute can be written to
        # we check the "editable" key in the schema field
        if check_writable:
            is_writable = schema_field[field_code].get(
                "editable", {}).get("value")
            if not is_writable:
                return False

        return schema_field
    except Exception:
        # shotgun_api3.shotgun.Fault: API schema_field_read()
        pass

    return False

create_ay_custom_attribs_in_sg_entity(sg_session, sg_entity_type, custom_attribs_map, custom_attribs_types)

Create AYON custom attributes in ShotGrid entities.

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a ShotGrid API Session.

required
sg_entities list

List of ShotGrid entities to create the fields in.

required
custom_attribs_map dict

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

required
custom_attribs_types dict

Dictionary that contains a tuple for each attribute containing the type of data and the scope of the attribute.

required
Source code in services/shotgrid_common/utils.py
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
def create_ay_custom_attribs_in_sg_entity(
    sg_session: shotgun_api3.Shotgun,
    sg_entity_type: str,
    custom_attribs_map: dict,
    custom_attribs_types: dict
):
    """Create AYON custom attributes in ShotGrid entities.

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a ShotGrid API Session.
        sg_entities (list): List of ShotGrid entities to create the fields in.
        custom_attribs_map (dict): Dictionary that maps names of attributes in
            AYON to ShotGrid equivalents.
        custom_attribs_types (dict): Dictionary that contains a tuple for each
            attribute containing the type of data and the scope of the
            attribute.
    """
    # Add all the custom attributes
    for sg_attrib in custom_attribs_map.values():

        data_scope = custom_attribs_types.get(sg_attrib)
        if not data_scope:
            continue

        data_type, ent_scope = data_scope

        # If SG entity type is not in the scope set on the attribute, skip it
        if sg_entity_type not in ent_scope:
            continue

        field_type = AYON_SHOTGRID_ATTRIBUTES_MAP[data_type]["name"]

        # First we simply validate whether the built-in attribute
        # already exists in the SG entity
        exists = check_sg_attribute_exists(
            sg_session,
            sg_entity_type,
            sg_attrib,
        )
        # If it doesn't exist, we create a custom attribute on the
        # SG entity by prefixing it with "sg_"
        if not exists:
            get_or_create_sg_field(
                sg_session,
                sg_entity_type,
                sg_attrib,
                field_type
            )

create_ay_fields_in_sg_entities(sg_session, sg_entities, custom_attribs_map, custom_attribs_types)

Create AYON fields in ShotGrid entities.

Some fields need to exist in the ShotGrid Entities, mainly the sg_ayon_id and sg_ayon_sync_status for the correct operation of the handlers.

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a ShotGrid API Session.

required
sg_entities list

List of ShotGrid entities to create the fields in.

required
custom_attribs_map dict

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

required
custom_attribs_types dict

Dictionary that contains a tuple for each attribute containing the type of data and the scope of the attribute.

required
Source code in services/shotgrid_common/utils.py
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
def create_ay_fields_in_sg_entities(
    sg_session: shotgun_api3.Shotgun,
    sg_entities: list,
    custom_attribs_map: dict,
    custom_attribs_types: dict
):
    """Create AYON fields in ShotGrid entities.

    Some fields need to exist in the ShotGrid Entities, mainly the `sg_ayon_id`
    and `sg_ayon_sync_status` for the correct operation of the handlers.

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a ShotGrid API Session.
        sg_entities (list): List of ShotGrid entities to create the fields in.
        custom_attribs_map (dict): Dictionary that maps names of attributes in
            AYON to ShotGrid equivalents.
        custom_attribs_types (dict): Dictionary that contains a tuple for each
            attribute containing the type of data and the scope of the attribute.
    """
    for sg_entity_type in sg_entities:
        get_or_create_sg_field(
            sg_session,
            sg_entity_type,
            "Ayon ID",
            "text",
            CUST_FIELD_CODE_ID,
        )

        get_or_create_sg_field(
            sg_session,
            sg_entity_type,
            "Ayon Sync Status",
            "list",
            CUST_FIELD_CODE_SYNC,
            field_properties={
                "name": "Ayon Sync Status",
                "description": "The Synchronization status with AYON.",
                "valid_values": ["Synced", "Failed", "Skipped"],
            }
        )

        # Add custom attributes to entity
        create_ay_custom_attribs_in_sg_entity(
            sg_session,
            sg_entity_type,
            custom_attribs_map,
            custom_attribs_types
        )

create_ay_fields_in_sg_project(sg_session, custom_attribs_map, custom_attribs_types)

Create AYON Project fields in ShotGrid.

This will create Project Unique attributes into ShotGrid.

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a ShotGrid API Session.

required
custom_attribs_map dict

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

required
custom_attribs_types dict

Dictionary that contains a tuple for each attribute containing the type of data and the scope of the attribute.

required
Source code in services/shotgrid_common/utils.py
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
def create_ay_fields_in_sg_project(
    sg_session: shotgun_api3.Shotgun,
    custom_attribs_map: dict,
    custom_attribs_types: dict
):
    """Create AYON Project fields in ShotGrid.

    This will create Project Unique attributes into ShotGrid.

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a ShotGrid API Session.
        custom_attribs_map (dict): Dictionary that maps names of attributes in
            AYON to ShotGrid equivalents.
        custom_attribs_types (dict): Dictionary that contains a tuple for each
            attribute containing the type of data and the scope of the
            attribute.
    """
    ayon_attribs_mapping = {
        attr_name: attr_dict["type"]
        for attr_name, attr_dict in get_attributes_for_type("folder").items()
    }
    for attribute, attribute_values in SG_PROJECT_ATTRS.items():
        sg_field_name = attribute_values["name"]
        sg_field_code = attribute_values["sg_field"]
        sg_field_type = attribute_values.get("type")
        sg_field_properties = {}

        if not sg_field_type:
            sg_field_type = ayon_attribs_mapping.get(attribute)

        if sg_field_type == "checkbox":
            sg_field_properties = {"default_value": False}

        get_or_create_sg_field(
            sg_session,
            "Project",
            sg_field_name,
            sg_field_type,
            field_code=sg_field_code,
            field_properties=sg_field_properties
        )

        # Add custom attributes to project
        create_ay_custom_attribs_in_sg_entity(
            sg_session,
            "Project",
            custom_attribs_map,
            custom_attribs_types
        )

create_new_ayon_entity(sg_session, entity_hub, parent_entity, sg_ay_dict)

Helper method to create entities in the EntityHub.

Task Creation

https://github.com/ynput/ayon-python-api/blob/30d7026/ayon_api/entity_hub.py#L284

Folder Creation

https://github.com/ynput/ayon-python-api/blob/30d7026/ayon_api/entity_hub.py#L254

Parameters:

Name Type Description Default
entity_hub EntityHub

The project's entity hub.

required
parent_entity Union[ProjectEntity, FolderEntity]

AYON parent entity.

required
sg_ay_dict dict

AYON ShotGrid entity to create.

required

Returns:

Type Description

FolderEntity|TaskEntity: Added task entity.

Source code in services/shotgrid_common/utils.py
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
def create_new_ayon_entity(
    sg_session: shotgun_api3.Shotgun,
    entity_hub: ayon_api.entity_hub.EntityHub,
    parent_entity: Union[ProjectEntity, FolderEntity],
    sg_ay_dict: Dict
):
    """Helper method to create entities in the EntityHub.

    Task Creation:
        https://github.com/ynput/ayon-python-api/blob/30d702618b58676c3708f09f131a0974a92e1002/ayon_api/entity_hub.py#L284

    Folder Creation:
        https://github.com/ynput/ayon-python-api/blob/30d702618b58676c3708f09f131a0974a92e1002/ayon_api/entity_hub.py#L254

    Args:
        entity_hub (ayon_api.EntityHub): The project's entity hub.
        parent_entity: AYON parent entity.
        sg_ay_dict (dict): AYON ShotGrid entity to create.

    Returns:
        FolderEntity|TaskEntity: Added task entity.
    """

    # Flow API return date as string, need to convert
    # them back as datetime in order to set to AYON.
    all_attrib_schemas = ayon_api.get_attributes_schema()
    for ay_attrib, attrib_value in sg_ay_dict["attribs"].items():
        attrib_schemas = [
            attr for attr in all_attrib_schemas["attributes"]
            if attr["name"] == ay_attrib
        ]
        if (
            attrib_schemas
            and attrib_schemas[0]["data"]["type"] == "datetime"
        ):
            value_as_date = datetime.datetime.strptime(
                attrib_value,
                "%Y-%m-%d",
            )
            sg_ay_dict["attribs"][ay_attrib] = value_as_date

    if sg_ay_dict["type"].lower() == "task":
        if parent_entity.entity_type == "project":
            log.warning("Cannot create task directly under project")
            return

        ay_entity = entity_hub.add_new_task(
            task_type=sg_ay_dict["task_type"],
            name=sg_ay_dict["name"],
            label=sg_ay_dict["label"],
            entity_id=sg_ay_dict["data"][CUST_FIELD_CODE_ID],
            parent_id=parent_entity.id,
            attribs=sg_ay_dict["attribs"],
            data=sg_ay_dict["data"]
        )

    elif sg_ay_dict["type"].lower() == "version":
        # SG doesn't have values for product_name and version (int)
        # we might create some assumption how to parsem out in the future
        log.warning(
            "Version creation from Flow is not implemented because "
            "Flow entity is much less strict than AYON product with reviewable "
            "(e.g. product name and integer are not mandatory in Flow)."
        )
        return
    elif sg_ay_dict["type"].lower() == "comment":
        handle_comment(sg_ay_dict, sg_session, entity_hub)
        return
    else:
        ay_entity = entity_hub.add_new_folder(
            folder_type=sg_ay_dict["folder_type"],
            name=sg_ay_dict["name"],
            label=sg_ay_dict["label"],
            entity_id=sg_ay_dict["data"][CUST_FIELD_CODE_ID],
            parent_id=parent_entity.id,
            attribs=sg_ay_dict["attribs"],
            data=sg_ay_dict["data"]
        )

    log.debug(f"Created new AYON entity: {ay_entity}")
    ay_entity.attribs.set(
        SHOTGRID_ID_ATTRIB,
        sg_ay_dict["attribs"].get(SHOTGRID_ID_ATTRIB, "")
    )
    ay_entity.attribs.set(
        SHOTGRID_TYPE_ATTRIB,
        sg_ay_dict["attribs"].get(SHOTGRID_TYPE_ATTRIB, "")
    )

    status = sg_ay_dict.get("status")
    if status:
        # Entity hub expects the statuses to be provided with the `name` and
        # not the `short_name` (which is what we get from SG) so we convert
        # the short name back to the long name before setting it
        status_mapping = {
            status.short_name: status.name
            for status in entity_hub.project_entity.statuses
        }
        new_status_name = status_mapping.get(status)
        if not new_status_name:
            log.warning(
                "Status with short name '%s' doesn't exist in project", status
            )
        else:
            try:
                # INFO: it was causing error so trying to set status directly
                ay_entity.status = new_status_name
            except ValueError as e:
                # `ValueError: Status ip is not available on project.`
                # NOTE: this doesn't really raise exception?
                log.warning(f"Status sync not implemented: {e}")

    assignees = sg_ay_dict.get("assignees")
    if assignees:
        ay_entity.assignees = assignees

    tags = sg_ay_dict.get("tags")
    if tags:
        ay_entity.tags = [tag["name"] for tag in tags]

    try:
        entity_hub.commit_changes()

    except Exception:
        log.error("AYON Entity could not be created", exc_info=True)

    else:

        # AssetCategory AYON entity do not have any equivalent in SG.
        if sg_ay_dict["attribs"][SHOTGRID_TYPE_ATTRIB] == "AssetCategory":
            return ay_entity

        try:
             sg_session.update(
                sg_ay_dict["attribs"][SHOTGRID_TYPE_ATTRIB],
                sg_ay_dict["attribs"][SHOTGRID_ID_ATTRIB],
                {
                    CUST_FIELD_CODE_ID: ay_entity.id
                }
            )

        except Exception:
            log.error(
                "Could not update SG %d entity with AYON id %r.",
                sg_ay_dict["attribs"][SHOTGRID_ID_ATTRIB],
                ay_entity.id
            )

    return ay_entity

create_new_sg_entity(ay_entity, sg_session, sg_project, sg_parent_entity, sg_enabled_entities, sg_project_code_field, custom_attribs_map, addon_settings, ay_project_name)

Helper method to create entities in Shotgrid.

Parameters:

Name Type Description Default
ay_entity Dict[str, Any]

The AYON entity.

required
sg_session Shotgun

The Shotgrid API session.

required
sg_project Dict[str, Any]

The Shotgrid Project.

required
sg_parent_entity Dict[str, str]

{"id": XX, "type": "Asset|Task.."}

required
sg_enabled_entities list

List of Shotgrid entities to be enabled.

required
custom_attribs_map dict

Dictionary of extra attributes to store in the SG entity.

required
addon_settings Dict[str, Any]

(Dict[str, Any]): settings from current version

required
ay_project_name str

(str): AYON project name, could be different from sg_project

required
Source code in services/shotgrid_common/utils.py
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
def create_new_sg_entity(
    ay_entity: Union[ProjectEntity, TaskEntity, FolderEntity, VersionEntity],
    sg_session: shotgun_api3.Shotgun,
    sg_project: Dict,
    sg_parent_entity: Dict,
    sg_enabled_entities: List[str],
    sg_project_code_field: str,
    custom_attribs_map: Dict[str, str],
    addon_settings: Dict[str, Any],
    ay_project_name: str
):
    """Helper method to create entities in Shotgrid.

    Args:
        ay_entity (Dict[str, Any]): The AYON entity.
        sg_session (shotgun_api3.Shotgun): The Shotgrid API session.
        sg_project (Dict[str, Any]): The Shotgrid Project.
        sg_parent_entity (Dict[str, str]): {"id": XX, "type": "Asset|Task.."}
        sg_enabled_entities (list): List of Shotgrid entities to be enabled.
        custom_attribs_map (dict): Dictionary of extra attributes to store in the SG entity.
        addon_settings: (Dict[str, Any]): settings from current version
        ay_project_name: (str): AYON project name, could be different from
            sg_project
    """
    if hasattr(ay_entity, "folder_type"):
        sg_type = ay_entity.folder_type
    else:
        sg_type = ay_entity.entity_type

    sg_parent_field = get_sg_entity_parent_field(
        sg_session, sg_project, sg_type.capitalize(), sg_enabled_entities)

    # generic data
    data = {
        "project": sg_project,
        CUST_FIELD_CODE_ID: ay_entity.id,
        CUST_FIELD_CODE_SYNC: "Synced",
    }

    # Task creation
    if ay_entity.entity_type == "task":
        step_query_filters = [["code", "is", ay_entity.task_type]]

        if sg_parent_entity["type"] in ["Asset", "Shot", "Episode", "Sequence"]:
            step_query_filters.append(
                ["entity_type", "is", sg_parent_entity["type"]]
            )

        task_step = sg_session.find_one(
            "Step",
            filters=step_query_filters,
            fields=["code", "name"],
        )
        if not task_step:
            raise ValueError(
                f"Unable to create Task {ay_entity.task_type} {ay_entity}\n"
                f"-> Shotgrid is missing Pipeline Step {ay_entity.task_type}"
            )

        sg_type = "Task"
        data["content"] = ay_entity.name
        data["entity"] = sg_parent_entity
        data["step"] = task_step

    # Asset creation
    elif (
        ay_entity.entity_type == "folder"
        and ay_entity.folder_type == "Asset"
    ):
        sg_type = "Asset"
        # get name form sg_parent_entity
        parent_entity_name = sg_parent_entity.get("name")

        if not parent_entity_name:
            # Try to get AssetCategory type name and use it as
            # SG asset type. If not found, use None.
            parent_entity = ay_entity.parent
            parent_entity_name = parent_entity.name
            asset_type = parent_entity_name.capitalize()
        else:
            asset_type = None

        log.debug(f"Creating Asset '{ay_entity.name}' of type '{asset_type}'")

        data["sg_asset_type"] = asset_type
        data["code"] = ay_entity.name
    elif ay_entity.entity_type == "version":
        sg_type = "Version"

        # this query shouldn't be necessary as we are reaching for attribs of
        # grandparent, but it seems that field is not returned correctly TODO
        folder_id = ay_entity.parent.parent.id
        ayon_asset = ayon_api.get_folder_by_id(
            ay_project_name, folder_id)

        if not ayon_asset:
            raise ValueError(f"Not found '{folder_id}'")

        ay_username = ay_entity.data.get("author")
        sg_user_id = get_sg_user_id(ay_username) if ay_username else -1
        if sg_user_id < 0:
            log.warning(
                f"{ay_username} is not synchronized, "
                f"Version will be created under script user."
            )
            data["description"] = f"Created in AYON by '{ay_username}'"
        else:
            data["user"] = {'type': 'HumanUser', 'id': sg_user_id}

        # sync associated task
        if ay_entity.task_id:
            task_data = ayon_api.get_task_by_id(
                ay_project_name,
                ay_entity.task_id
            )
            sg_task = task_data["attrib"].get(SHOTGRID_ID_ATTRIB)
            if sg_task:
                data["sg_task"] = {"type": "Task", "id": int(sg_task)}

        # sync comment for description
        data["description"] = ay_entity.attribs.get("comment")

        # sync productType as version type
        product_data =  ayon_api.get_product_by_id(
            ay_project_name,
            ay_entity.product_id
        )
        sg_version_field = sg_session.schema_field_read(
            "Version", "sg_version_type")["sg_version_type"]
        sg_valid_values = (
            sg_version_field["properties"]["valid_values"]["value"]
        )

        if product_data["productType"] in sg_valid_values:
            data["sg_version_type"] = product_data["productType"]

        # sync first/last frames
        frame_start = ay_entity.attribs.get("frameStart") or 0
        frame_end = ay_entity.attribs.get("frameEnd") or 0
        handle_start = ay_entity.attribs.get("handleStart") or 0
        handle_end = ay_entity.attribs.get("handleEnd") or 0

        frame_in = frame_start - handle_start
        frame_out = frame_end + handle_end

        data["sg_first_frame"]  = frame_in
        data["sg_last_frame"] = frame_out

        data["frame_count"] = frame_out - frame_in + 1
        data["frame_range"] = '-'.join([str(frame_in), str(frame_out)])

        product_name = ay_entity.parent.name
        version_str = str(ay_entity.version).zfill(3)
        version_name = f"{product_name}_v{version_str}"

        data[sg_parent_field] = sg_parent_entity
        data["code"] = version_name

    # Folder creation
    else:
        sg_type = ay_entity.folder_type

        # If parent field is different than project, add parent field to
        # data dictionary. Each project might have different parent fields
        # defined on each entity types. This way we secure that we are
        # always creating the entity with the correct parent field.
        if (
            sg_parent_field != "project"
            and sg_parent_entity["type"] != "Project"
        ):
            data[sg_parent_field] = sg_parent_entity
        data["code"] = ay_entity.name

    # Set status
    if ay_entity.status:
        entity = ay_entity
        project_entity = None
        while entity.parent:
            entity = entity.parent
            if entity.entity_type == "project":
                project_entity = entity
                break

        if project_entity:
            ay_statuses = {
                status.name: status.short_name
                for status in project_entity.statuses
            }
            ay_status = ay_statuses.get(ay_entity.status)
            if ay_status and ay_status in get_sg_statuses(sg_session, sg_type):
                data["sg_status_list"] = ay_status

    # Fill up data with any extra attributes from AYON we want to sync to SG
    data |= get_sg_custom_attributes_data(
        sg_session,
        ay_entity.attribs.to_dict(),
        sg_type,
        custom_attribs_map
    )

    try:
        sg_entity = sg_session.create(sg_type, data)
    except Exception as e:
        log.error(
            f"Unable to create SG entity {sg_type} with data: {data}")
        raise e

    compatibility_settings = addon_settings.get("compatibility_settings", {})
    default_task_type = compatibility_settings.get("default_task_type")

    return get_sg_entity_as_ay_dict(
        sg_session,
        sg_entity["type"],
        sg_entity["id"],
        sg_project_code_field,
        default_task_type,
        custom_attribs_map=custom_attribs_map
    )

create_sg_entities_in_ay(project_entity, sg_session, shotgrid_project, sg_enabled_entities)

Ensure AYON has all the SG Steps (to use as task types) and Folder types.

Parameters:

Name Type Description Default
project_entity ProjectEntity

The ProjectEntity for a given project.

required
sg_session Shotgun

Shotgun Session object.

required
shotgrid_project dict

The project owning the Tasks.

required
sg_enabled_entities list

The enabled entities.

required
Source code in services/shotgrid_common/utils.py
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
def create_sg_entities_in_ay(
    project_entity: ProjectEntity,
    sg_session: shotgun_api3.Shotgun,
    shotgrid_project: dict,
    sg_enabled_entities: list,
):
    """Ensure AYON has all the SG Steps (to use as task types) and Folder types.

    Args:
        project_entity (ProjectEntity): The ProjectEntity for a given project.
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        shotgrid_project (dict): The project owning the Tasks.
        sg_enabled_entities (list): The enabled entities.
    """

    # Types of SG entities to ignore as AYON folders
    ignored_folder_types = {"task", "version"}

    # Find ShotGrid Entities that are to be treated as folders
    sg_folder_entities = [
        {"name": entity_type}
        for entity_type, _ in get_sg_project_enabled_entities(
            sg_session,
            shotgrid_project,
            sg_enabled_entities
        ) if entity_type.lower() not in ignored_folder_types
    ]

    new_folder_types = sg_folder_entities + project_entity.folder_types
    # So we can have a specific folder for AssetCategory
    new_folder_types.extend([
        {"name": "AssetCategory"},
        {"name": "Folder"}
    ])

    # Make sure list items are unique
    new_folder_types = list({
        entity['name']: entity
        for entity in new_folder_types
    }.values())
    project_entity.folder_types = new_folder_types

    # Add ShotGrid Statuses to AYON Project Entity
    ay_statuses = {
        status.short_name.lower(): status.name.lower()
        for status in list(project_entity.statuses)
    }
    ay_status_codes = list(ay_statuses.keys())
    ay_status_names = list(ay_statuses.values())
    for sg_entity_type in sg_enabled_entities:
        if sg_entity_type == "Project":
            # Skipping statuses from SG project as they are irrelevant in AYON
            continue
        for status_code, status_name in get_sg_statuses(sg_session, sg_entity_type).items():
            if status_code.lower() not in ay_status_codes:
                if status_name.lower() in ay_status_names:
                    status_name += " (from SG)"
                project_entity.statuses.create(status_name, short_name=status_code)
                ay_status_codes.append(status_code)

    # Add Project task types by querying ShotGrid Pipeline steps
    sg_steps = [
        {"name": step[0], "shortName": step[1]}
        for step in get_sg_pipeline_steps(
            sg_session,
            shotgrid_project,
            sg_enabled_entities
        )
    ]
    new_task_types = sg_steps + project_entity.task_types
    new_task_types = list({
        task['name']: task
        for task in new_task_types
    }.values())
    project_entity.task_types = new_task_types

    return sg_folder_entities, sg_steps

get_ayon_name_by_sg_id(sg_user_id)

Returns AYON user name for particular sg_user_id

Calls SG addon endpoint to query 'users' table limit need to loop through all users.

Returns: (Optional[str])

Source code in services/shotgrid_common/utils.py
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
def get_ayon_name_by_sg_id(sg_user_id):
    """Returns AYON user name for particular `sg_user_id`

    Calls SG addon endpoint to query 'users' table limit need to loop through
    all users.

    Args:
        sg_user_id (str)
    Returns:
        (Optional[str])
    """
    addon_name = ayon_api.get_service_addon_name()
    addon_version = ayon_api.get_service_addon_version()
    variant = ayon_api.get_default_settings_variant()
    endpoint_url = (
        f"addons/{addon_name}/{addon_version}/"
        f"get_ayon_name_by_sg_id/{sg_user_id}"
        f"?variant={variant}"
    )

    response = ayon_api.get(endpoint_url)
    if response.status_code != 200:
        print(response.content)
        raise RuntimeError(response.text)

    return response.data

get_event_hash(event_topic, event_id)

Create a SHA-256 hash from the event topic and event ID.

Parameters:

Name Type Description Default
event_topic str

The event topic.

required
event_id int

The event ID.

required

Returns:

Name Type Description
str str

The SHA-256 hash.

Source code in services/shotgrid_common/utils.py
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def get_event_hash(event_topic: str, event_id: int) -> str:
    """Create a SHA-256 hash from the event topic and event ID.

    Arguments:
        event_topic (str): The event topic.
        event_id (int): The event ID.

    Returns:
        str: The SHA-256 hash.
    """
    data = {
        "event_topic": event_topic,
        "event_id": event_id,
    }
    json_data = json.dumps(data)
    return hashlib.sha256(json_data.encode("utf-8")).hexdigest()

get_logger(name)

Return a logger instance with the given name.

Source code in services/shotgrid_common/utils.py
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
def get_logger(name: str) -> logging.Logger:
    """Return a logger instance with the given name."""
    if name in _loggers:
        return _loggers[name]

    # get environment variable DEBUG level
    log_level = os.environ.get("LOGLEVEL", "INFO").upper()

    logger = logging.Logger(name)
    _loggers[name] = logger
    # create console handler and set level to debug
    ch = logging.StreamHandler()
    ch.setLevel(log_level)

    formatting_str = (
        "%(asctime)s.%(msecs)03d %(levelname)s: %(message)s"
    )

    if log_level == "DEBUG":
        formatting_str = (
            "%(asctime)s.%(msecs)03d | %(module)s | %(funcName)s | "
            "%(levelname)s: %(message)s"
        )

    # create formatter
    formatter = logging.Formatter(
        fmt=formatting_str,
        datefmt="%Y-%m-%d %H:%M:%S",
    )

    # add formatter to ch
    ch.setFormatter(formatter)

    # add ch to logger
    logger.addHandler(ch)

    return logger

get_or_create_sg_field(sg_session, sg_entity_type, field_name, field_type, field_code=None, field_properties={})

Return a field from a ShotGrid Entity or create it if it doesn't exist.

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a ShotGrid API Session.

required
sg_entity_type str

The ShotGrid entity type the field belongs to.

required
field_name str

The ShotGrid field name, displayed in the UI.

required
field_type str

The type of ShotGrid field.

required
field_code Optional[str]

The ShotGrid field code, inferred from the the field_name if not provided.

None
field_properties Optional[dict]

Some fields allow extra properties, these can be defined with this argument.

{}

Returns:

Name Type Description
attribute_exists str

The Field name (code).

Source code in services/shotgrid_common/utils.py
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
def get_or_create_sg_field(
    sg_session: shotgun_api3.Shotgun,
    sg_entity_type: str,
    field_name: str,
    field_type: str,
    field_code: Optional[str] = None,
    field_properties: Optional[dict] = {}
):
    """Return a field from a ShotGrid Entity or create it if it doesn't exist.

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a ShotGrid API Session.
        sg_entity_type (str): The ShotGrid entity type the field belongs to.
        field_name (str): The ShotGrid field name, displayed in the UI.
        field_type (str): The type of ShotGrid field.
        field_code (Optional[str]): The ShotGrid field code, inferred from the
            the `field_name` if not provided.
        field_properties (Optional[dict]): Some fields allow extra properties,
            these can be defined with this argument.

    Returns:
        attribute_exists (str): The Field name (code).
    """
    if not field_code:
        field_code = f"sg_{field_name.lower().replace(' ', '_')}"

    attribute_exists = check_sg_attribute_exists(
        sg_session, sg_entity_type, field_code)

    if not attribute_exists:

        try:
            attribute_exists = sg_session.schema_field_create(
                sg_entity_type,
                field_type,
                field_name,
                properties=field_properties,
            )
            return attribute_exists
        except Exception:
            log.error(
                "Can't create ShotGrid field "
                f"{sg_entity_type} > {field_code}.",
                exc_info=True
            )

    return attribute_exists

get_reparenting_from_settings(entity_hub, sg_ay_dict, addon_settings)

AYON settings can change the hierarchy mapping via settings: - root_relocate: move the hierarchy in-place under a different root - type grouping: group all entities of a same time under a common location.

Parameters:

Name Type Description Default
entity_hub EntityHub

The project's entity hub.

required
parent_entity

Ayon parent entity.

required
sg_ay_dict dict

The ShotGrid entity ready for Ayon consumption.

required

Returns:

Type Description

dict. The new parent entity if modified by the settings, None otherwise.

Source code in services/shotgrid_common/utils.py
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
def get_reparenting_from_settings(entity_hub, sg_ay_dict, addon_settings):
    """ AYON settings can change the hierarchy mapping via settings:
    - root_relocate: move the hierarchy in-place under a different root
    - type grouping: group all entities of a same time under a common location.

    Args:
        entity_hub (ayon_api.EntityHub): The project's entity hub.
        parent_entity: Ayon parent entity.
        sg_ay_dict (dict): The ShotGrid entity ready for Ayon consumption.

    Returns:
        dict. The new parent entity if modified by the settings, None otherwise.
    """
    transfer_type = _get_parenting_transfer_type(addon_settings)

    # No re-parenting settings enabled,
    # no hierarchy changes must be made.
    if transfer_type is None:
        return None

    sg_type = sg_ay_dict["folder_type"]

    # AssetCategory is an AYON specific entity type.
    # Apply same logic as Asset when reparenting from SG.
    if sg_type == "AssetCategory":
        sg_type = "Asset"

    folders_and_types = _get_parents_and_types(
        addon_settings,
        transfer_type,
        sg_type
    )

    # No special rules set for this entity type
    # in the settings, no hierarchy changes must be made.
    if not folders_and_types:
        return None

    return _get_special_category(
        entity_hub,
        sg_ay_dict,
        folders_and_types=folders_and_types
    )

get_sg_custom_attributes_data(sg_session, ay_attribs, sg_entity_type, custom_attribs_map)

Get a dictionary with all the extra attributes we want to sync to SG

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a Shotgrid API Session.

required
ay_attribs dict

Dictionary that contains the ground truth data of attributes that we want to sync to SG.

required
sg_entity_type str

ShotGrid Entity type.

required
custom_attribs_map dict

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

required
Source code in services/shotgrid_common/utils.py
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
def get_sg_custom_attributes_data(
    sg_session: shotgun_api3.Shotgun,
    ay_attribs: dict,
    sg_entity_type: str,
    custom_attribs_map: dict,
) -> dict:
    """Get a dictionary with all the extra attributes we want to sync to SG

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a Shotgrid API Session.
        ay_attribs (dict): Dictionary that contains the ground truth data of
            attributes that we want to sync to SG.
        sg_entity_type (str): ShotGrid Entity type.
        custom_attribs_map (dict): Dictionary that maps names of attributes in
            AYON to ShotGrid equivalents.
    """
    data_to_update = {}
    for ay_attrib, sg_attrib in custom_attribs_map.items():
        attrib_value = ay_attribs.get(ay_attrib)
        if attrib_value is None:
            continue

        # try it first without `sg_` prefix since some are built-in
        exists = check_sg_attribute_exists(
            sg_session, sg_entity_type, sg_attrib, check_writable=True
        )
        # and then with the prefix
        if not exists:
            sg_attrib = f"sg_{sg_attrib}"
            exists = check_sg_attribute_exists(
                sg_session, sg_entity_type, sg_attrib, check_writable=True
            )

        if exists:

            try:
                value_as_date = datetime.datetime.fromisoformat(str(attrib_value))

            except (ValueError, TypeError):
                value_as_date = None

            # AYON attribute value converts as date,
            # confirm targeted SG field is also of type date.
            if value_as_date:
                schema_field = sg_session.schema_field_read(
                    sg_entity_type,
                    field_name=sg_attrib
                )
                data_type = schema_field[sg_attrib]["data_type"]["value"]
                if data_type == "date":
                    # AYON returns its date as isoformat, but FLOW API expects its date
                    # formatted as YYY-MM-DD
                    attrib_value = value_as_date.strftime("%Y-%m-%d")

            data_to_update[sg_attrib] = attrib_value

    return data_to_update

get_sg_entities(sg_session, sg_project, sg_enabled_entities, project_code_field, custom_attribs_map, addon_settings, extra_fields=None)

Get all available entities within a ShotGrid Project.

We check with ShotGrid to see what entities are enabled in a given project, then we build two dictionaries, one containing all entities with their ID as key and the representation as the value, and another dictionary where we store all the children on an entity, the key is the parent entity, and the value a list of it's children; all this by querying all the existing entities in a project for the enabled entities.

Note: Asset Categories in ShotGrid aren't entities per se, or at least not queryable from the API, so we treat them as folders.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
sg_project dict

The ShotGrid project to query its entities.

required
sg_enabled_entities list

List of ShotGrid entities to query.

required
project_code_field str

The ShotGrid project code field.

required
custom_attribs_map dict

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

required
addon_settings dict

Settings

required
extra_fields list

List of extra fields to pass to the query.

None

Returns:

Type Description
dict

tuple( entities_by_id (dict): A dict containing all entities with their ID as key. entities_by_parent_id (dict): A dict containing all entities that have children.

dict

)

Source code in services/shotgrid_common/utils.py
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
def get_sg_entities(
    sg_session: shotgun_api3.Shotgun,
    sg_project: dict,
    sg_enabled_entities: list,
    project_code_field: str,
    custom_attribs_map: dict,
    addon_settings: dict,
    extra_fields: Optional[list] = None,
) -> tuple[dict, dict]:
    """Get all available entities within a ShotGrid Project.

    We check with ShotGrid to see what entities are enabled in a given project,
    then we build two dictionaries, one containing all entities with their ID
    as key and the representation as the value, and another dictionary where we
    store all the children on an entity, the key is the parent entity, and the
    value a list of it's children; all this by querying all the existing
    entities in a project for the enabled entities.

    Note: Asset Categories in ShotGrid aren't entities per se, or at least not
    queryable from the API, so we treat them as folders.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        sg_project (dict): The ShotGrid project to query its entities.
        sg_enabled_entities (list): List of ShotGrid entities to query.
        project_code_field (str): The ShotGrid project code field.
        custom_attribs_map (dict): Dictionary that maps names of attributes in
            AYON to ShotGrid equivalents.
        addon_settings (dict): Settings
        extra_fields (list): List of extra fields to pass to the query.

    Returns:
        tuple(
            entities_by_id (dict): A dict containing all entities with
                their ID as key.
            entities_by_parent_id (dict): A dict containing all entities
                that have children.
        )

    """
    compatibility_settings = addon_settings.get("compatibility_settings", {})
    default_task_type = compatibility_settings.get("default_task_type")

    query_fields = list(SG_COMMON_ENTITY_FIELDS)

    if extra_fields and isinstance(extra_fields, list):
        query_fields += extra_fields

    for sg_attrib in custom_attribs_map.values():
        query_fields.extend([f"sg_{sg_attrib}", sg_attrib])

    project_enabled_entities = get_sg_project_enabled_entities(
        sg_session,
        sg_project,
        sg_enabled_entities
    )

    if not project_code_field:
        project_code_field = "code"

    sg_ay_dicts = {
        sg_project["id"]: _sg_to_ay_dict(
            sg_project,
            project_code_field,
            custom_attribs_map,
            default_task_type
        ),
    }

    sg_ay_dicts_parents: Dict[str, set] = (
        collections.defaultdict(set)
    )

    for enabled_entity in project_enabled_entities:
        entity_name, parent_field = enabled_entity

        if entity_name == "Reply":  # Reply doesn't have link to project
            continue

        sg_entities = sg_session.find(
            entity_name,
            filters=[["project", "is", sg_project]],
            fields=query_fields,
        )

        for sg_entity in sg_entities:
            parent_id = sg_project["id"]

            if (
                parent_field != "project"
                and sg_entity[parent_field]
                and entity_name != "Asset"
            ):

                sg_parent = sg_entity[parent_field]

                # Parenting in Project tracking settings can
                # point to a non-entity entry (e.g. AYON Sync status).
                # Set parent id only if defined parent is a valid entity.
                if isinstance(sg_parent, dict) and sg_parent.get("id"):
                    parent_id = sg_parent["id"]
                    parent_type = sg_parent["type"]
                    parent_id = f'{parent_type}_{parent_id}'

            # Reparent the current SG Asset under an AssetCategory ?
            elif (
                entity_name == "Asset"
                and sg_entity.get("sg_asset_type")
            ):
                # Asset Categories (sg_asset_type) are not entities
                # (or at least aren't queryable) in ShotGrid
                # thus here we create common folders.
                asset_category = sg_entity["sg_asset_type"]
                # asset category entity name
                cat_ent_name = slugify_string(asset_category).lower()

                if cat_ent_name not in sg_ay_dicts:
                    asset_category_entity = {
                        "label": asset_category,
                        "name": cat_ent_name,
                        "attribs": {
                            SHOTGRID_ID_ATTRIB: slugify_string(
                                asset_category).lower(),
                            SHOTGRID_TYPE_ATTRIB: "AssetCategory",
                        },
                        "data": {
                            CUST_FIELD_CODE_ID: None,
                            CUST_FIELD_CODE_SYNC: None,
                        },
                        "type": "folder",
                        "folder_type": "AssetCategory",
                    }
                    sg_ay_dicts[cat_ent_name] = asset_category_entity
                    sg_ay_dicts_parents[sg_project["id"]].add(cat_ent_name)

                parent_id = cat_ent_name

            _add_task_assignees(sg_entity)

            sg_ay_dict = _sg_to_ay_dict(
                sg_entity,
                project_code_field,
                custom_attribs_map,
                default_task_type
            )

            sg_id = (
                f'{sg_ay_dict["attribs"][SHOTGRID_TYPE_ATTRIB]}_'
                f'{sg_ay_dict["attribs"][SHOTGRID_ID_ATTRIB]}'
            )
            sg_ay_dicts[sg_id] = sg_ay_dict
            sg_ay_dicts_parents[parent_id].add(sg_id)

    return sg_ay_dicts, sg_ay_dicts_parents

get_sg_entity_as_ay_dict(sg_session, sg_type, sg_id, project_code_field, default_task_type, custom_attribs_map=None, extra_fields=None, retired_only=False)

Get a ShotGrid entity, and morph it to an AYON compatible one.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
sg_type str

The ShotGrid entity type.

required
sg_id int

ShotGrid ID of the entity to query.

required
project_code_field str

The ShotGrid project code field.

required
default_task_type str

The default task type to use.

required
custom_attribs_map Optional[dict]

Dictionary that maps names of attributes in AYON to ShotGrid equivalents.

None
extra_fields Optional[list]

List of optional fields to query.

None
retired_only bool

Whether to return only retired entities.

False

Returns: new_entity (dict): The ShotGrid entity ready for AYON consumption.

Source code in services/shotgrid_common/utils.py
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
def get_sg_entity_as_ay_dict(
    sg_session: shotgun_api3.Shotgun,
    sg_type: str,
    sg_id: int,
    project_code_field: str,
    default_task_type: str,
    custom_attribs_map: Optional[Dict[str, str]] = None,
    extra_fields: Optional[list] = None,
    retired_only: Optional[bool] = False,
) -> dict:
    """Get a ShotGrid entity, and morph it to an AYON compatible one.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        sg_type (str): The ShotGrid entity type.
        sg_id (int): ShotGrid ID of the entity to query.
        project_code_field (str): The ShotGrid project code field.
        default_task_type (str): The default task type to use.
        custom_attribs_map (Optional[dict]): Dictionary that maps names of
            attributes in AYON to ShotGrid equivalents.
        extra_fields (Optional[list]): List of optional fields to query.
        retired_only (bool): Whether to return only retired entities.
    Returns:
        new_entity (dict): The ShotGrid entity ready for AYON consumption.
    """
    query_fields = list(SG_COMMON_ENTITY_FIELDS)
    if extra_fields and isinstance(extra_fields, list):
        query_fields.extend(extra_fields)
    else:
        extra_fields = []

    # If custom attributes are passed, query each of them
    # NOTE: we query both with the prefix "sg_" and without
    # to account for the fact that some attributes are built-in
    # and some aren't in SG
    if custom_attribs_map:
        for sg_attrib in custom_attribs_map.values():
            query_fields.extend([f"sg_{sg_attrib}", sg_attrib])

    if project_code_field not in query_fields:
        query_fields.append(project_code_field)

    sg_entity = sg_session.find_one(
        sg_type,
        filters=[["id", "is", sg_id]],
        fields=query_fields,
        retired_only=retired_only
    )

    if not sg_entity:
        return {}

    _add_task_assignees(sg_entity)

    sg_ay_dict = _sg_to_ay_dict(
        sg_entity, project_code_field, custom_attribs_map, default_task_type
    )

    for field in extra_fields:
        sg_value = sg_entity.get(field)
        # If no value in SG entity skip
        if sg_value is None:
            continue

        sg_ay_dict["data"][field] = sg_value

    return sg_ay_dict

get_sg_entity_parent_field(sg_session, sg_project, sg_entity_type, sg_enabled_entities)

Find the ShotGrid entity field that points to its parent.

This is handy since there is really no way to tell the parent entity of a ShotGrid entity unless you don't know this field, and it can change based on projects and their Tracking Settings.

Parameters:

Name Type Description Default
sg_session Shotgun

ShotGrid Session object.

required
sg_project dict

ShotGrid Project dict representation.

required
sg_entity_type str

ShotGrid Entity type.

required

Returns:

Name Type Description
sg_parent_field str

The field that points to the entity parent.

Source code in services/shotgrid_common/utils.py
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
def get_sg_entity_parent_field(
    sg_session: shotgun_api3.Shotgun,
    sg_project: dict,
    sg_entity_type: str,
    sg_enabled_entities: list
) -> str:
    """Find the ShotGrid entity field that points to its parent.

    This is handy since there is really no way to tell the parent entity of
    a ShotGrid entity unless you don't know this field, and it can change based
    on projects and their Tracking Settings.

    Args:
        sg_session (shotgun_api3.Shotgun): ShotGrid Session object.
        sg_project (dict): ShotGrid Project dict representation.
        sg_entity_type (str): ShotGrid Entity type.

    Returns:
        sg_parent_field (str): The field that points to the entity parent.
    """
    sg_parent_field = ""

    for entity_tuple in get_sg_project_enabled_entities(
        sg_session, sg_project, sg_enabled_entities
    ):
        entity_type, parent_field = entity_tuple

        if entity_type == sg_entity_type:
            sg_parent_field = parent_field

    return sg_parent_field

get_sg_missing_ay_attributes(sg_session)

Ensure all the AYON required fields are present in ShotGrid.

Parameters:

Name Type Description Default
sg_session Shotgun

Instance of a ShotGrid API Session.

required

Returns:

Name Type Description
missing_attrs list

Contains any missing attribute, if any.

Source code in services/shotgrid_common/utils.py
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
def get_sg_missing_ay_attributes(sg_session: shotgun_api3.Shotgun):
    """ Ensure all the AYON required fields are present in ShotGrid.

    Args:
        sg_session (shotgun_api3.Shotgun): Instance of a ShotGrid API Session.

    Returns:
        missing_attrs (list): Contains any missing attribute, if any.
    """
    missing_attrs = []
    for ayon_attr, attr_dict in SG_PROJECT_ATTRS.items():
        try:
            sg_session.schema_field_read(
                "Project",
                field_name=f"sg_{ayon_attr}"
            )
        except Exception:
            # shotgun_api3.shotgun.Fault: API schema_field_read()
            missing_attrs.append(ayon_attr)

    return missing_attrs

get_sg_pipeline_steps(sg_session, shotgrid_project, sg_enabled_entities)

Get all pipeline steps on a ShotGrid project.

Parameters:

Name Type Description Default
sg_session Shotgun

ShotGrid Session object.

required
shotgrid_project dict

The project owning the Pipeline steps.

required

Returns: sg_steps (list): ShotGrid Project Pipeline Steps list.

Source code in services/shotgrid_common/utils.py
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
def get_sg_pipeline_steps(
    sg_session: shotgun_api3.Shotgun,
    shotgrid_project: dict,
    sg_enabled_entities: list,
) -> list:
    """ Get all pipeline steps on a ShotGrid project.

    Args:
        sg_session (shotgun_api3.Shotgun): ShotGrid Session object.
        shotgrid_project (dict): The project owning the Pipeline steps.
    Returns:
        sg_steps (list): ShotGrid Project Pipeline Steps list.
    """
    sg_steps = []
    enabled_entities = get_sg_project_enabled_entities(
        sg_session,
        shotgrid_project,
        sg_enabled_entities
    )

    pipeline_steps = sg_session.find(
        "Step",
        filters=[{
                "filter_operator": "any",
                "filters": [
                    ["entity_type", "is", entity]
                    for entity, _ in enabled_entities
                ]
            }],
        fields=["code", "short_name", "entity_type"]
    )

    for step in pipeline_steps:
        sg_steps.append((step["code"], step["short_name"].lower()))

    sg_steps = list(set(sg_steps))
    return sg_steps

get_sg_project_by_id(sg_session, project_id, extra_fields=None)

Find a project in ShotGrid by its id.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
project_id int

The project ID to look for.

required
extra_fields Optional[list]

List of optional fields to query.

None

Returns: sg_project (dict): ShotGrid Project dict.

Source code in services/shotgrid_common/utils.py
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
def get_sg_project_by_id(
    sg_session: shotgun_api3.Shotgun,
    project_id: int,
    extra_fields: Optional[list] = None
) -> dict:
    """ Find a project in ShotGrid by its id.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        project_id (int): The project ID to look for.
        extra_fields (Optional[list]): List of optional fields to query.
    Returns:
        sg_project (dict): ShotGrid Project dict.
     """
    common_fields = list(SG_COMMON_ENTITY_FIELDS)

    if extra_fields:
        common_fields.extend(extra_fields)

    sg_project = sg_session.find_one(
        "Project",
        [["id", "is", project_id]],
        fields=common_fields,
    )

    if not sg_project:
        raise ValueError(f"Unable to find project {project_id} in ShotGrid.")

    return sg_project

get_sg_project_by_name(sg_session, project_name, custom_fields=None)

Find a project in ShotGrid by its name.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
project_name str

The project name to look for.

required

Returns: sg_project (dict): ShotGrid Project dict.

Source code in services/shotgrid_common/utils.py
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
def get_sg_project_by_name(
    sg_session: shotgun_api3.Shotgun,
    project_name: str,
    custom_fields: list = None,
) -> dict:
    """ Find a project in ShotGrid by its name.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        project_name (str): The project name to look for.
    Returns:
        sg_project (dict): ShotGrid Project dict.
    """
    common_fields = ["id", "code", "name", "sg_status"]

    if custom_fields and isinstance(custom_fields, list):
        common_fields += custom_fields

    sg_project = sg_session.find_one(
        "Project",
        [["name", "is", project_name]],
        fields=common_fields,
    )

    if not sg_project:
        raise ValueError(f"Unable to find project {project_name} in ShotGrid.")

    return sg_project

get_sg_project_enabled_entities(sg_session, sg_project, sg_enabled_entities)

Function to get all enabled entities in a project.

ShotGrid allows a lot of flexibility when it comes to hierarchies, here we find all the enabled entity type (Shots, Sequence, etc) in a specific project and provide the configured field that points to the parent entity.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
project_name str

The project name to look for.

required
sg_enabled_entities list

The SG entities enabled from the settings.

required

Returns:

Name Type Description
project_entities list[tuple(entity type, parent field)]

List of enabled entities names and their respective parent field.

Source code in services/shotgrid_common/utils.py
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
def get_sg_project_enabled_entities(
    sg_session: shotgun_api3.Shotgun,
    sg_project: dict,
    sg_enabled_entities: list,
) -> list:
    """Function to get all enabled entities in a project.

    ShotGrid allows a lot of flexibility when it comes to hierarchies, here we
    find all the enabled entity type (Shots, Sequence, etc) in a specific
    project and provide the configured field that points to the parent entity.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        project_name (str): The project name to look for.
        sg_enabled_entities (list): The SG entities enabled from the settings.

    Returns:
        project_entities (list[tuple(entity type, parent field)]): List of
            enabled entities names and their respective parent field.
    """
    sg_project = sg_session.find_one(
        "Project",
        filters=[["id", "is", sg_project["id"]]],
        fields=["tracking_settings", "name", "code"]
    )

    if not sg_project:
        log.error(
            f"Unable to find {sg_project} in the ShotGrid instance."
        )
        return []

    sg_project_schema = sg_session.schema_entity_read(
        project_entity=sg_project
    )

    project_navigation = sg_project["tracking_settings"]["navchains"]
    # explicit parent fields - not part of hierarchy
    project_navigation["Task"] = "entity"
    project_navigation["Version"] = "entity"

    project_entities = []

    for sg_entity_type in sg_enabled_entities:
        if sg_entity_type == "Project":
            continue

        is_entity_enabled = sg_project_schema.get(
            sg_entity_type, {}
        ).get("visible", {}).get("value", False)

        if not is_entity_enabled:
            log.warning(
                "%s is enabled in AYON settings for project "
                "but hidden in Flow tracking settings for project %r. "
                "It'll be ignored, please check "
                "your configuration.",
                sg_entity_type,
                sg_project.get("name") or sg_project.get("code"),
            )

        else:
            parent_field = project_navigation.get(sg_entity_type, None)

            if parent_field and parent_field != "__flat__":
                if "," in parent_field:
                    # This catches instances where the Hierarchy is set to
                    # something like "Seq > Scene > Shot" which returns
                    # a string like so: 'sg_scene,Scene.sg_sequence' and
                    # confusing enough we want the first element to be
                    # the parent.
                    parent_field = parent_field.split(",")[0]

                project_entities.append((
                    sg_entity_type,
                    parent_field.replace(f"{sg_entity_type}.", "")
                ))
            else:
                project_entities.append((sg_entity_type, "project"))

    return project_entities

get_sg_statuses(sg_session, sg_entity_type=None)

Get all supported ShotGrid Statuses.

Parameters:

Name Type Description Default
sg_session Shotgun

ShotGrid Session object.

required
sg_entity_type str

ShotGrid Entity type.

None

Returns:

Name Type Description
sg_statuses dict[str, str]

ShotGrid Project Statuses dictionary mapping the status short code and its long name.

Source code in services/shotgrid_common/utils.py
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
def get_sg_statuses(
    sg_session: shotgun_api3.Shotgun,
    sg_entity_type: Optional[str] = None
) -> dict:
    """ Get all supported ShotGrid Statuses.

    Args:
        sg_session (shotgun_api3.Shotgun): ShotGrid Session object.
        sg_entity_type (str): ShotGrid Entity type.

    Returns:
        sg_statuses (dict[str, str]): ShotGrid Project Statuses dictionary
            mapping the status short code and its long name.
    """
    # If given an entity type, we filter out the statuses by just the ones
    # supported by that entity
    # NOTE: this is a limitation in AYON as the statuses are global and not
    # per entity
    sg_statuses = {}
    if sg_entity_type:
        if sg_entity_type == "Project":
            status_field = "sg_status"
        elif sg_entity_type == "Playlist":
            status_field = "sg_playlist_status"
        else:
            status_field = "sg_status_list"
        try:
            entity_status = sg_session.schema_field_read(sg_entity_type, status_field)
            sg_statuses = entity_status[status_field]["properties"]["display_values"]["value"]
        except shotgun_api3.shotgun.Fault:
            log.warning(
                f"Unable to get status field '{status_field}' for {sg_entity_type} "
                "in Flow."
            )
        return sg_statuses

    sg_statuses = {
        status["code"]: status["name"]
        for status in sg_session.find("Status", [], fields=["name", "code"])
    }
    return sg_statuses

get_sg_tags(sg_session)

Get all tags on a ShotGrid project.

Parameters:

Name Type Description Default
sg_session Shotgun

ShotGrid Session object.

required
sg_entity_type str

ShotGrid Entity type.

required

Returns:

Name Type Description
sg_tags dict[str, str]

ShotGrid Project tags dictionary mapping the tag name to its id.

Source code in services/shotgrid_common/utils.py
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
def get_sg_tags(
    sg_session: shotgun_api3.Shotgun
) -> dict:
    """ Get all tags on a ShotGrid project.

    Args:
        sg_session (shotgun_api3.Shotgun): ShotGrid Session object.
        sg_entity_type (str): ShotGrid Entity type.

    Returns:
        sg_tags (dict[str, str]): ShotGrid Project tags dictionary
            mapping the tag name to its id.
    """
    sg_tags = {
        tags["name"].lower(): tags["id"]
        for tags in sg_session.find("Tag", [], fields=["name", "id"])
    }
    return sg_tags

get_sg_user_by_id(sg_session, user_id, extra_fields=None)

Find a user in ShotGrid by its id.

Parameters:

Name Type Description Default
sg_session Shotgun

Shotgun Session object.

required
user_id int

The user ID to look for.

required
extra_fields Optional[list]

List of optional fields to query.

None

Returns: sg_project (dict): ShotGrid Project dict.

Source code in services/shotgrid_common/utils.py
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
def get_sg_user_by_id(
    sg_session: shotgun_api3.Shotgun,
    user_id: int,
    extra_fields: Optional[list] = None
) -> dict:
    """ Find a user in ShotGrid by its id.

    Args:
        sg_session (shotgun_api3.Shotgun): Shotgun Session object.
        user_id (int): The user ID to look for.
        extra_fields (Optional[list]): List of optional fields to query.
    Returns:
        sg_project (dict): ShotGrid Project dict.
     """
    common_fields = list(SG_COMMON_ENTITY_FIELDS)

    if extra_fields:
        common_fields.extend(extra_fields)

    sg_user = sg_session.find_one(
        "HumanUser",
        [["id", "is", user_id]],
        fields=common_fields,
    )

    if not sg_user:
        raise ValueError(f"Unable to find HumanUser {user_id} in ShotGrid.")

    return sg_user

get_sg_user_id(ayon_username)

Returns the ShotGrid user ID for a given AYON username.

Queries AYON's user database to retrieve the associated ShotGrid user ID for a specified AYON username. If no association is found, the method returns -1.

Parameters:

Name Type Description Default
ayon_username str

The username in AYON.

required

Returns:

Name Type Description
int [int]

The corresponding ShotGrid user ID, or -1 if not found.

-1 returned as caching is used to differentiate between missing user in cache AND user without SG id

Source code in services/shotgrid_common/utils.py
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
def get_sg_user_id(ayon_username: str) -> [int]:
    """Returns the ShotGrid user ID for a given AYON username.

    Queries AYON's user database to retrieve the associated ShotGrid user ID
    for a specified AYON username. If no association is found, the method
    returns `-1`.

    Args:
        ayon_username (str): The username in AYON.

    Returns:
        int: The corresponding ShotGrid user ID, or `-1` if not found.

    -1 returned as caching is used to differentiate between missing user in
    cache AND user without SG id
    """
    ayon_user = ayon_api.get_user(ayon_username)
    if not ayon_user or not ayon_user["data"].get("sg_user_id"):
        sg_user_id = -1
    else:
        sg_user_id = ayon_user["data"]["sg_user_id"]
    return sg_user_id

handle_comment(sg_ay_dict, sg_session, entity_hub)

Transforms content and links from SG to matching AYON structures.

Source code in services/shotgrid_common/utils.py
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
def handle_comment(sg_ay_dict, sg_session, entity_hub):
    """Transforms content and links from SG to matching AYON structures."""
    sg_note_id = sg_ay_dict["attribs"][SHOTGRID_ID_ATTRIB]
    sg_note, sg_note_id = _get_sg_chat_msg(sg_note_id, sg_session, "Note")

    if not sg_note:
        log.warning(f"Couldn't find note '{sg_note_id}'")
        return

    ayon_user_name = _get_ayon_user_name(sg_note["user"])

    ay_parent_entity = _get_sg_note_parent_entity(entity_hub, sg_note, sg_session)
    if not ay_parent_entity:
        log.warning(f"Cannot find parent for comment '{sg_note_id}'")
        return

    content = _get_content_with_notifications(sg_note)

    project_name = entity_hub.project_name

    sg_ayon_id = sg_ay_dict["data"].get(CUST_FIELD_CODE_ID)
    ayon_comment = None
    if sg_ayon_id:
        ayon_comment = ayon_api.get_activity_by_id(project_name, sg_ayon_id)

    if not ayon_comment:
        ay_activity_id = _add_comment(
            sg_session,
            project_name,
            ay_parent_entity["id"],
            ay_parent_entity["entity_type"],
            ayon_user_name,
            content,
            sg_note,
        )
    else:
        ay_activity_id = _update_comment(
            sg_session,
            project_name,
            ay_parent_entity,
            ay_parent_entity["entity_type"],
            ayon_comment,
            sg_note,
        )
    #updates SG with AYON comment id
    sg_session.update(
        sg_ay_dict["attribs"].get(SHOTGRID_TYPE_ATTRIB, ""),
        sg_ay_dict["attribs"].get(SHOTGRID_ID_ATTRIB, ""),
        {
            CUST_FIELD_CODE_ID: ay_activity_id
        }
    )

update_ay_entity_custom_attributes(ay_entity, sg_ay_dict, custom_attribs_map, values_to_update=None, ay_project=None)

Update AYON entity custom attributes from ShotGrid dictionary

Source code in services/shotgrid_common/utils.py
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
def update_ay_entity_custom_attributes(
    ay_entity: Union[ProjectEntity, FolderEntity, TaskEntity],
    sg_ay_dict: dict,
    custom_attribs_map: dict,
    values_to_update: Optional[list] = None,
    ay_project: ProjectEntity = None,
):
    """Update AYON entity custom attributes from ShotGrid dictionary"""

    # Check renaming through label
    if (
        sg_ay_dict["type"].lower() != "version"
        and sg_ay_dict["label"]
        and (ay_entity.label or ay_entity.get_name()) != sg_ay_dict["label"]
    ):
        ay_entity.label = sg_ay_dict["label"]

    # Loop over custom attributes and detect changes.
    for ay_attrib, _ in custom_attribs_map.items():
        if values_to_update and ay_attrib not in values_to_update:
            continue

        attrib_value = sg_ay_dict["attribs"].get(ay_attrib, sg_ay_dict.get(ay_attrib, None))

        if attrib_value is None:
            continue

        if ay_attrib == "tags":
            ay_entity.tags = [tag["name"] for tag in attrib_value]
        elif ay_attrib == "status":
            # Entity hub expects the statuses to be provided with the `name` and
            # not the `short_name` (which is what we get from SG) so we convert
            # the short name back to the long name before setting it
            status_mapping = {
                status.short_name: status
                for status in ay_project.statuses
            }
            new_status = status_mapping.get(attrib_value)
            if new_status and ay_entity.entity_type in new_status.scope:
                ay_entity.status = new_status.name
            else:
                logging.warning(
                    f"Status '{attrib_value}' not available"
                    f" for {ay_entity.entity_type}."
                )
        elif ay_attrib == "assignees":
            if hasattr(ay_entity, "assignees"):
                ay_entity.assignees = attrib_value
            else:
                logging.warning(
                    "Assignees sync not available with current"
                    " ayon-python-api version."
                )
        else:

            # SG API returns date values as string.
            # Attempt to detect date field values.
            try:
                value_as_date = datetime.datetime.strptime(
                    str(attrib_value),
                    "%Y-%m-%d",
                )

            except (ValueError, TypeError):
                value_as_date = None

            # Field value matches a valid date,
            if value_as_date:
                all_attrib_schemas = ayon_api.get_attributes_schema()
                attrib_schemas = [
                    attr for attr in all_attrib_schemas["attributes"]
                    if attr["name"] == ay_attrib
                ]
                # confirm target AYON attribute is of type datetime.
                if not (
                    attrib_schemas
                    and attrib_schemas[0]["data"]["type"] == "datetime"
                ):
                    continue

                # Check is a different date
                current_set_date = ay_entity.attribs.get(ay_attrib)
                value_as_utc = value_as_date.replace(
                    tzinfo=datetime.timezone.utc).date()
                if (
                    current_set_date
                    and datetime.datetime.fromisoformat(current_set_date).date()
                    == value_as_utc
                ):
                    continue

                attrib_value = value_as_date

            ay_entity.attribs.set(ay_attrib, attrib_value)

update_movie_paths(sg_session, ayon_entity_hub, payload)

Uses prepare sg_ field to store sg_path_to_ to particular Version

Source code in services/shotgrid_common/utils.py
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
def update_movie_paths(
    sg_session: shotgun_api3.Shotgun,
    ayon_entity_hub: ayon_api.entity_hub.EntityHub,
    payload: dict
):
    """Uses prepare sg_* field to store sg_path_to_* to particular Version"""
    ay_version_id = payload.pop("versionId")
    log.info(f"Updating paths '{ay_version_id}'")

    ay_version_entity = ayon_entity_hub.get_version_by_id(ay_version_id)
    if not ay_version_entity:
        raise ValueError(
            "Event has a non existent version entity "
            f"'{ay_version_id}'"
        )

    sg_version_id = ay_version_entity.attribs.get(SHOTGRID_ID_ATTRIB)
    sg_version_type = ay_version_entity.attribs.get(SHOTGRID_TYPE_ATTRIB)

    if not sg_version_id:
        raise ValueError(f"Version '{ay_version_id} not yet synched to SG.")

    sg_session.update(
        sg_version_type,
        sg_version_id,
        payload
    )